diet-guard/app/test/services/sync_settings_test.dart

160 lines
5.1 KiB
Dart
Raw Normal View History

import 'package:diet_guard_app/services/sync_settings.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../fake_secure_storage.dart';
void main() {
// installFakeSecureStorage touches the test binary messenger, which needs
// the binding up first (widget tests get this for free via testWidgets).
TestWidgetsFlutterBinding.ensureInitialized();
test(
'load returns the kuhyx/diet-guard-sync defaults on a fresh install',
() async {
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage();
final s = await SyncSettings.load();
expect(s.owner, 'kuhyx');
expect(s.repo, 'diet-guard-sync');
expect(s.token, '');
Add OAuth device flow, background notifications, and fix AGP9 release crash (Milestones 3–4) M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect GitHub" button that runs the device-code flow; tapping with no client id now opens a setup dialog (instructions + inline paste field) rather than a buried inline hint. Bakes in the app's own OAuth App client id so fresh installs work with zero manual config. Auto-syncs immediately after connect. Verified end-to-end on the real phone: OAuth flow → token saved → PC's 48-entry log merged in (confirmed via food-bank vs manual source labels in History). M4 – Background meal-slot notifications: WorkManager periodic task (15 min floor) checks for overdue slots and posts/cancels notifications via flutter_local_notifications. New permissions: POST_NOTIFICATIONS, WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, INTERNET (was missing — latent sync bug). "Disable battery optimization" button in Settings. Verified on real phone: WorkManager registered, forced run posted a real notification ("Meal not logged / You haven't logged your 16:00 meal yet."), isolated to background path (only caller is the WorkManager dispatcher, not any foreground lifecycle hook). AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources to true for release even with no proguard config; R8 stripped WorkDatabase_Impl's reflection-only constructor, crashing every launch with NoSuchMethodException. Explicitly disabled both flags in build.gradle.kts. Verified via dexdump (constructor present) and on-device launch (no crash). Proper R8 keep rules are the long-term fix; tracked. 177 tests, flutter analyze clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:29:23 +02:00
expect(s.clientId, SyncSettings.defaultClientId);
},
);
test('save stores the token in the keystore, not in prefs', () async {
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage();
await const SyncSettings(owner: 'me', repo: 'notes', token: 'tok').save();
// Token must not linger in plaintext prefs once secured.
final prefs = await SharedPreferences.getInstance();
expect(prefs.getString('sync.token'), isNull);
final s = await SyncSettings.load();
expect(s.owner, 'me');
expect(s.repo, 'notes');
expect(s.token, 'tok');
});
test('load reads the token straight from the keystore', () async {
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage(initial: {'sync.token': 'fromKeystore'});
final s = await SyncSettings.load();
expect(s.token, 'fromKeystore');
});
test('load migrates a legacy plaintext token into the keystore', () async {
SharedPreferences.setMockInitialValues({'sync.token': 'legacy'});
installFakeSecureStorage();
final s = await SyncSettings.load();
expect(s.token, 'legacy');
// The plaintext copy is dropped once the secure write succeeds, and the
// value now resolves from the keystore on the next load.
final prefs = await SharedPreferences.getInstance();
expect(prefs.getString('sync.token'), isNull);
final again = await SyncSettings.load();
expect(again.token, 'legacy');
});
test(
'load keeps the plaintext token when no secret service is available',
() async {
SharedPreferences.setMockInitialValues({'sync.token': 'plain'});
installFakeSecureStorage(throwing: true);
final s = await SyncSettings.load();
expect(s.token, 'plain');
// Never drop the only copy when the keystore write can't be confirmed.
final prefs = await SharedPreferences.getInstance();
expect(prefs.getString('sync.token'), 'plain');
},
);
test('save falls back to plaintext prefs when the keystore fails', () async {
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage(throwing: true);
await const SyncSettings(owner: 'o', repo: 'r', token: 'tok').save();
final prefs = await SharedPreferences.getInstance();
expect(prefs.getString('sync.token'), 'tok');
});
test('save with an empty token clears the keystore entry', () async {
// Seed a keystore token, then save an empty token: it must be deleted
// and no plaintext copy written.
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage(initial: {'sync.token': 'old'});
await const SyncSettings(owner: 'o', repo: 'r', token: '').save();
final prefs = await SharedPreferences.getInstance();
expect(prefs.getString('sync.token'), isNull);
final s = await SyncSettings.load();
expect(s.token, '');
});
test('isConfigured requires owner, repo and token', () {
expect(
const SyncSettings(owner: 'o', repo: 'r', token: 't').isConfigured,
isTrue,
);
expect(
const SyncSettings(owner: 'o', repo: 'r', token: '').isConfigured,
isFalse,
);
});
Add OAuth device flow, background notifications, and fix AGP9 release crash (Milestones 3–4) M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect GitHub" button that runs the device-code flow; tapping with no client id now opens a setup dialog (instructions + inline paste field) rather than a buried inline hint. Bakes in the app's own OAuth App client id so fresh installs work with zero manual config. Auto-syncs immediately after connect. Verified end-to-end on the real phone: OAuth flow → token saved → PC's 48-entry log merged in (confirmed via food-bank vs manual source labels in History). M4 – Background meal-slot notifications: WorkManager periodic task (15 min floor) checks for overdue slots and posts/cancels notifications via flutter_local_notifications. New permissions: POST_NOTIFICATIONS, WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, INTERNET (was missing — latent sync bug). "Disable battery optimization" button in Settings. Verified on real phone: WorkManager registered, forced run posted a real notification ("Meal not logged / You haven't logged your 16:00 meal yet."), isolated to background path (only caller is the WorkManager dispatcher, not any foreground lifecycle hook). AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources to true for release even with no proguard config; R8 stripped WorkDatabase_Impl's reflection-only constructor, crashing every launch with NoSuchMethodException. Explicitly disabled both flags in build.gradle.kts. Verified via dexdump (constructor present) and on-device launch (no crash). Proper R8 keep rules are the long-term fix; tracked. 177 tests, flutter analyze clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:29:23 +02:00
test('canUseDeviceFlow is true only when a client id is set', () {
expect(
const SyncSettings(owner: 'o', repo: 'r', token: '').canUseDeviceFlow,
isFalse,
);
expect(
const SyncSettings(
owner: 'o',
repo: 'r',
token: '',
clientId: 'cid',
).canUseDeviceFlow,
isTrue,
);
});
test('save persists the client id and load reads it back', () async {
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage();
await const SyncSettings(
owner: 'o',
repo: 'r',
token: '',
clientId: 'cid123',
).save();
final s = await SyncSettings.load();
expect(s.clientId, 'cid123');
});
test('copyWith overrides only the given fields', () {
Add OAuth device flow, background notifications, and fix AGP9 release crash (Milestones 3–4) M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect GitHub" button that runs the device-code flow; tapping with no client id now opens a setup dialog (instructions + inline paste field) rather than a buried inline hint. Bakes in the app's own OAuth App client id so fresh installs work with zero manual config. Auto-syncs immediately after connect. Verified end-to-end on the real phone: OAuth flow → token saved → PC's 48-entry log merged in (confirmed via food-bank vs manual source labels in History). M4 – Background meal-slot notifications: WorkManager periodic task (15 min floor) checks for overdue slots and posts/cancels notifications via flutter_local_notifications. New permissions: POST_NOTIFICATIONS, WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, INTERNET (was missing — latent sync bug). "Disable battery optimization" button in Settings. Verified on real phone: WorkManager registered, forced run posted a real notification ("Meal not logged / You haven't logged your 16:00 meal yet."), isolated to background path (only caller is the WorkManager dispatcher, not any foreground lifecycle hook). AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources to true for release even with no proguard config; R8 stripped WorkDatabase_Impl's reflection-only constructor, crashing every launch with NoSuchMethodException. Explicitly disabled both flags in build.gradle.kts. Verified via dexdump (constructor present) and on-device launch (no crash). Proper R8 keep rules are the long-term fix; tracked. 177 tests, flutter analyze clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:29:23 +02:00
const base = SyncSettings(
owner: 'o',
repo: 'r',
token: 't',
clientId: 'cid',
);
final next = base.copyWith(token: 'new');
expect(next.owner, 'o');
expect(next.repo, 'r');
expect(next.token, 'new');
Add OAuth device flow, background notifications, and fix AGP9 release crash (Milestones 3–4) M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect GitHub" button that runs the device-code flow; tapping with no client id now opens a setup dialog (instructions + inline paste field) rather than a buried inline hint. Bakes in the app's own OAuth App client id so fresh installs work with zero manual config. Auto-syncs immediately after connect. Verified end-to-end on the real phone: OAuth flow → token saved → PC's 48-entry log merged in (confirmed via food-bank vs manual source labels in History). M4 – Background meal-slot notifications: WorkManager periodic task (15 min floor) checks for overdue slots and posts/cancels notifications via flutter_local_notifications. New permissions: POST_NOTIFICATIONS, WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, INTERNET (was missing — latent sync bug). "Disable battery optimization" button in Settings. Verified on real phone: WorkManager registered, forced run posted a real notification ("Meal not logged / You haven't logged your 16:00 meal yet."), isolated to background path (only caller is the WorkManager dispatcher, not any foreground lifecycle hook). AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources to true for release even with no proguard config; R8 stripped WorkDatabase_Impl's reflection-only constructor, crashing every launch with NoSuchMethodException. Explicitly disabled both flags in build.gradle.kts. Verified via dexdump (constructor present) and on-device launch (no crash). Proper R8 keep rules are the long-term fix; tracked. 177 tests, flutter analyze clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:29:23 +02:00
expect(next.clientId, 'cid');
// No-arg copy exercises the `?? this.x` fallback on every field.
final clone = base.copyWith();
expect(clone.owner, 'o');
expect(clone.repo, 'r');
expect(clone.token, 't');
Add OAuth device flow, background notifications, and fix AGP9 release crash (Milestones 3–4) M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect GitHub" button that runs the device-code flow; tapping with no client id now opens a setup dialog (instructions + inline paste field) rather than a buried inline hint. Bakes in the app's own OAuth App client id so fresh installs work with zero manual config. Auto-syncs immediately after connect. Verified end-to-end on the real phone: OAuth flow → token saved → PC's 48-entry log merged in (confirmed via food-bank vs manual source labels in History). M4 – Background meal-slot notifications: WorkManager periodic task (15 min floor) checks for overdue slots and posts/cancels notifications via flutter_local_notifications. New permissions: POST_NOTIFICATIONS, WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, INTERNET (was missing — latent sync bug). "Disable battery optimization" button in Settings. Verified on real phone: WorkManager registered, forced run posted a real notification ("Meal not logged / You haven't logged your 16:00 meal yet."), isolated to background path (only caller is the WorkManager dispatcher, not any foreground lifecycle hook). AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources to true for release even with no proguard config; R8 stripped WorkDatabase_Impl's reflection-only constructor, crashing every launch with NoSuchMethodException. Explicitly disabled both flags in build.gradle.kts. Verified via dexdump (constructor present) and on-device launch (no crash). Proper R8 keep rules are the long-term fix; tracked. 177 tests, flutter analyze clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:29:23 +02:00
expect(clone.clientId, 'cid');
});
}