mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 18:07:03 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
146 lines
4.2 KiB
Dart
146 lines
4.2 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:crdt_sync/crdt_sync.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:todo/cli/cli_session.dart';
|
|
import 'package:todo/data/note.dart';
|
|
|
|
import 'fake_rtdb.dart';
|
|
|
|
void main() {
|
|
late Directory tmp;
|
|
late FakeRtdb db;
|
|
late String seed;
|
|
|
|
setUp(() {
|
|
tmp = Directory.systemTemp.createTempSync('cli_session_test');
|
|
db = FakeRtdb();
|
|
seed = writeSeedCredentials(tmp).path;
|
|
});
|
|
tearDown(() => tmp.deleteSync(recursive: true));
|
|
|
|
Future<CliSession> open(String name, {String? seedPath}) => CliSession.open(
|
|
'${tmp.path}/$name',
|
|
seedCredentials: seedPath ?? seed,
|
|
httpClient: db.client,
|
|
);
|
|
|
|
Note note(String id) {
|
|
final now = DateTime.utc(2026);
|
|
return Note(
|
|
id: id,
|
|
text: 'Title\nbody',
|
|
priority: Priority.medium,
|
|
status: Status.todo,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
);
|
|
}
|
|
|
|
group('defaultDataDir', () {
|
|
test('prefers XDG_DATA_HOME', () {
|
|
expect(
|
|
defaultDataDir({'XDG_DATA_HOME': '/x', 'HOME': '/h'}),
|
|
'/x/todo-cli',
|
|
);
|
|
});
|
|
|
|
test('falls back to ~/.local/share', () {
|
|
expect(defaultDataDir({'HOME': '/h'}), '/h/.local/share/todo-cli');
|
|
expect(
|
|
defaultDataDir({'XDG_DATA_HOME': '', 'HOME': '/h'}),
|
|
'/h/.local/share/todo-cli',
|
|
);
|
|
});
|
|
|
|
test('survives a missing HOME', () {
|
|
expect(defaultDataDir({}), '.local/share/todo-cli');
|
|
});
|
|
});
|
|
|
|
group('open', () {
|
|
test('creates an owner-only data dir and persists one node id', () async {
|
|
final first = await open('data');
|
|
final nodeId = first.repository.nodeId;
|
|
await first.close();
|
|
final dir = Directory('${tmp.path}/data');
|
|
expect(dir.statSync().mode & 0x1ff, 448); // 0700
|
|
expect(
|
|
File('${dir.path}/${CliSession.nodeIdFileName}').readAsStringSync(),
|
|
'$nodeId\n',
|
|
);
|
|
|
|
final second = await open('data');
|
|
expect(second.repository.nodeId, nodeId);
|
|
await second.close();
|
|
});
|
|
|
|
test('mints a fresh id when the stored one is blank', () async {
|
|
Directory('${tmp.path}/blank').createSync();
|
|
File(
|
|
'${tmp.path}/blank/${CliSession.nodeIdFileName}',
|
|
).writeAsStringSync(' \n');
|
|
final session = await open('blank');
|
|
expect(session.repository.nodeId, isNotEmpty);
|
|
await session.close();
|
|
});
|
|
|
|
test('fails without a Firebase session, naming the seed', () async {
|
|
final missing = '${tmp.path}/missing.json';
|
|
await expectLater(
|
|
open('nosession', seedPath: missing),
|
|
throwsA(
|
|
isA<FirebaseAuthError>().having(
|
|
(e) => e.toString(),
|
|
'message',
|
|
contains(missing),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('sync', () {
|
|
test('a pull-only sync never writes to the database', () async {
|
|
final session = await open('pull');
|
|
await session.repository.upsert(note('n1'));
|
|
final result = await session.sync(push: false);
|
|
expect(result.pushed, isTrue); // SyncService reports its own intent.
|
|
expect(db.methods, isNot(contains('PUT')));
|
|
expect(db.files, isEmpty);
|
|
await session.close();
|
|
});
|
|
|
|
test('a push publishes the log, then skips an unchanged one', () async {
|
|
final session = await open('push');
|
|
await session.repository.upsert(note('n1'));
|
|
final first = await session.sync(push: true);
|
|
expect(first.pushed, isTrue);
|
|
final node = session.repository.nodeId;
|
|
expect(db.files.keys, contains('todo-sync/notes/$node~2Ejson'));
|
|
expect(db.files.keys, contains('todo-sync/revs/$node'));
|
|
expect(
|
|
File('${tmp.path}/push/${CliSession.syncStateFileName}').existsSync(),
|
|
isTrue,
|
|
);
|
|
|
|
final second = await session.sync(push: true);
|
|
expect(second.pushed, isFalse);
|
|
await session.close();
|
|
});
|
|
|
|
test('a second node sees the first node note', () async {
|
|
final a = await open('a');
|
|
await a.repository.upsert(note('shared'));
|
|
await a.sync(push: true);
|
|
await a.close();
|
|
|
|
final b = await open('b');
|
|
final result = await b.sync(push: false);
|
|
expect(result.mergedDevices, 1);
|
|
expect((await b.repository.listNotes()).single.id, 'shared');
|
|
await b.close();
|
|
});
|
|
});
|
|
}
|