Remote sync of webdav

This commit is contained in:
Courtney Arnold 2026-08-13 17:05:10 -05:00
parent f01186df79
commit 483fbeafb6
44 changed files with 4842 additions and 975 deletions

View file

@ -0,0 +1,115 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fuel_tax_tracker/services/cloud/webdav_provider.dart';
const _multistatusBody = '''<?xml version="1.0"?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>/</D:href>
<D:propstat>
<D:prop><D:resourcetype><D:collection/></D:resourcetype></D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>''';
/// Confirms sign-in survives a cold app restart: [WebDavProvider] persists
/// verified credentials to secure storage on [signInWithCredentials], and a
/// *fresh* instance (an in-memory field reset, standing in for a relaunched
/// app) can restore the session from them via [attemptSilentSignIn] — the
/// gap flagged in README's "Known limitations" and fixed here.
void main() {
late HttpServer server;
late String serverUrl;
const username = 'alice';
const password = 'hunter2';
setUp(() async {
FlutterSecureStoragePlatform.instance = _FakeSecureStoragePlatform();
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
serverUrl = 'http://127.0.0.1:${server.port}/';
unawaited(server.forEach((request) async {
final auth = request.headers.value('authorization');
final expected = 'Basic ${base64Encode(utf8.encode('$username:$password'))}';
if (request.method == 'PROPFIND' && auth == expected) {
request.response.statusCode = 207;
request.response.headers.contentType = ContentType('application', 'xml');
request.response.write(_multistatusBody);
} else if (request.method == 'PROPFIND') {
request.response.statusCode = 401;
} else {
request.response.statusCode = 404;
}
await request.response.close();
}));
});
tearDown(() async {
await server.close(force: true);
});
test('signInWithCredentials persists credentials that a fresh instance can restore', () async {
final first = WebDavProvider();
await first.signInWithCredentials(serverUrl: serverUrl, username: username, password: password);
expect(first.isSignedIn, isTrue);
// A brand new instance has no in-memory session — simulates the app
// process having been killed and relaunched.
final afterRestart = WebDavProvider();
expect(afterRestart.isSignedIn, isFalse);
final restored = await afterRestart.attemptSilentSignIn();
expect(restored, isTrue);
expect(afterRestart.isSignedIn, isTrue);
expect(afterRestart.accountLabel, first.accountLabel);
});
test('attemptSilentSignIn returns false, not throws, when nothing was ever stored', () async {
final provider = WebDavProvider();
expect(await provider.attemptSilentSignIn(), isFalse);
expect(provider.isSignedIn, isFalse);
});
test('signOut clears stored credentials so a later restart no longer restores', () async {
final first = WebDavProvider();
await first.signInWithCredentials(serverUrl: serverUrl, username: username, password: password);
await first.signOut();
final afterRestart = WebDavProvider();
expect(await afterRestart.attemptSilentSignIn(), isFalse);
});
}
class _FakeSecureStoragePlatform extends FlutterSecureStoragePlatform {
final Map<String, String> _store = {};
@override
Future<void> write({required String key, required String value, required Map<String, String> options}) async {
_store[key] = value;
}
@override
Future<String?> read({required String key, required Map<String, String> options}) async => _store[key];
@override
Future<bool> containsKey({required String key, required Map<String, String> options}) async =>
_store.containsKey(key);
@override
Future<void> delete({required String key, required Map<String, String> options}) async {
_store.remove(key);
}
@override
Future<Map<String, String>> readAll({required Map<String, String> options}) async => Map.of(_store);
@override
Future<void> deleteAll({required Map<String, String> options}) async => _store.clear();
}