mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-07-04 11:43:10 +02:00
Notes list & filtering: - Text-search filter plus independent date-range filters for both created and last-updated (AND-combined), a priority filter, and a new status filter. Default view hides Done/Abandoned and renders as "unfiltered" (no badge for the default state); fixed badge clipping. - NoteSort options wired into the list UI; watchCount() for the "N saved". Status & priority: - New Status enum (toDo/inProgress/Done/Abandoned) as a settable + filterable attribute on every note, with capture-screen dropdown. - Removed "None" priority: every note is Low/Medium/High, default Medium. Schema migration v2->v3 rewrites legacy priority 0 -> Medium. Export / import: - NotesMarkdown round-trippable single-file format with HTML-comment markers. - Settings "Export notes" (mobile share sheet / desktop writes ~/todo/BACKLOG.md) and "Import notes" (file picker + safe newer-wins merge by id). Structured template: - Every new note pre-fills the richer what/where/must/nice/out/done/depends/ estimate/refs scaffold. Tests: - New fast (~5s), deterministic suite via FakeNoteRepository (no DB timers) and injected http/file-selector/url-launcher fakes. 86 tests, 96.2% line coverage (note.dart & sync_service.dart at 100%, settings 98.7%). Mobile-only share branch excluded via coverage:ignore (unreachable on the Linux test host). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
4.2 KiB
Dart
156 lines
4.2 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:todo/sync/github_device_auth.dart';
|
|
|
|
/// Builds an auth instance whose polls resolve instantly (no real waiting).
|
|
GitHubDeviceAuth authWith(http.Client client) => GitHubDeviceAuth(
|
|
clientId: 'test-client-id',
|
|
httpClient: client,
|
|
delay: (_) => Future<void>.value(),
|
|
);
|
|
|
|
const _device = DeviceCodeResponse(
|
|
deviceCode: 'dev-123',
|
|
userCode: 'WXYZ-1234',
|
|
verificationUri: 'https://github.com/login/device',
|
|
interval: 1,
|
|
expiresIn: 900,
|
|
);
|
|
|
|
void main() {
|
|
test('requestDeviceCode parses the device + user code', () async {
|
|
final client = MockClient((req) async {
|
|
expect(req.url.toString(), contains('login/device/code'));
|
|
expect(req.bodyFields['client_id'], 'test-client-id');
|
|
expect(req.bodyFields['scope'], 'repo');
|
|
return http.Response(
|
|
jsonEncode({
|
|
'device_code': 'dev-123',
|
|
'user_code': 'WXYZ-1234',
|
|
'verification_uri': 'https://github.com/login/device',
|
|
'interval': 5,
|
|
'expires_in': 900,
|
|
}),
|
|
200,
|
|
);
|
|
});
|
|
|
|
final res = await authWith(client).requestDeviceCode();
|
|
expect(res.deviceCode, 'dev-123');
|
|
expect(res.userCode, 'WXYZ-1234');
|
|
expect(res.verificationUri, 'https://github.com/login/device');
|
|
});
|
|
|
|
test('pollForToken returns the token after authorization_pending', () async {
|
|
var calls = 0;
|
|
final client = MockClient((req) async {
|
|
calls++;
|
|
// Pending on the first two polls, then success.
|
|
if (calls < 3) {
|
|
return http.Response(
|
|
jsonEncode({'error': 'authorization_pending'}),
|
|
200,
|
|
);
|
|
}
|
|
return http.Response(
|
|
jsonEncode({'access_token': 'gho_abc', 'token_type': 'bearer'}),
|
|
200,
|
|
);
|
|
});
|
|
|
|
final token = await authWith(client).pollForToken(_device);
|
|
expect(token, 'gho_abc');
|
|
expect(calls, 3);
|
|
});
|
|
|
|
test('pollForToken obeys slow_down and still succeeds', () async {
|
|
var calls = 0;
|
|
final client = MockClient((req) async {
|
|
calls++;
|
|
if (calls == 1) {
|
|
return http.Response(
|
|
jsonEncode({'error': 'slow_down', 'interval': 1}),
|
|
200,
|
|
);
|
|
}
|
|
return http.Response(jsonEncode({'access_token': 'gho_xyz'}), 200);
|
|
});
|
|
|
|
final token = await authWith(client).pollForToken(_device);
|
|
expect(token, 'gho_xyz');
|
|
});
|
|
|
|
test('pollForToken throws on access_denied', () async {
|
|
final client = MockClient(
|
|
(req) async => http.Response(
|
|
jsonEncode({'error': 'access_denied', 'error_description': 'no'}),
|
|
200,
|
|
),
|
|
);
|
|
|
|
expect(
|
|
() => authWith(client).pollForToken(_device),
|
|
throwsA(
|
|
isA<DeviceAuthException>().having(
|
|
(e) => e.code,
|
|
'code',
|
|
'access_denied',
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('pollForToken honors slow_down then succeeds', () async {
|
|
var calls = 0;
|
|
final client = MockClient((req) async {
|
|
calls++;
|
|
if (calls == 1) {
|
|
return http.Response(
|
|
jsonEncode({'error': 'slow_down', 'interval': 0}),
|
|
200,
|
|
);
|
|
}
|
|
return http.Response(jsonEncode({'access_token': 'gho_ok'}), 200);
|
|
});
|
|
|
|
expect(await authWith(client).pollForToken(_device), 'gho_ok');
|
|
expect(calls, 2);
|
|
});
|
|
|
|
test('pollForToken throws on an unexpected response shape', () async {
|
|
final client = MockClient(
|
|
(_) async => http.Response(jsonEncode({'foo': 'bar'}), 200),
|
|
);
|
|
expect(
|
|
() => authWith(client).pollForToken(_device),
|
|
throwsA(isA<DeviceAuthException>()),
|
|
);
|
|
});
|
|
|
|
test('pollForToken throws when the device code has expired', () async {
|
|
final client = MockClient(
|
|
(_) async => http.Response(jsonEncode({'access_token': 'x'}), 200),
|
|
);
|
|
const expired = DeviceCodeResponse(
|
|
deviceCode: 'd',
|
|
userCode: 'u',
|
|
verificationUri: 'v',
|
|
interval: 1,
|
|
expiresIn: 0, // deadline is now → loop body never runs
|
|
);
|
|
expect(
|
|
() => authWith(client).pollForToken(expired),
|
|
throwsA(
|
|
isA<DeviceAuthException>().having(
|
|
(e) => e.code,
|
|
'code',
|
|
'expired_token',
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|