feat: 重构会话管理与提醒通知系统
This commit is contained in:
@@ -75,7 +75,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('home route screen is wrapped with ChatBloc provider', () {
|
||||
final widget = buildHomeRouteScreen();
|
||||
final widget = buildHomeRouteScreen(
|
||||
const AuthAuthenticated(
|
||||
user: AuthUser(id: 'u1', phone: '13800138000'),
|
||||
),
|
||||
);
|
||||
|
||||
expect(widget, isA<BlocProvider<ChatBloc>>());
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/app/services/session_scope_manager.dart';
|
||||
import 'package:social_app/data/cache/cache_scope.dart';
|
||||
import 'package:social_app/data/cache/cache_store.dart';
|
||||
|
||||
class _RecordingHybridCacheStore extends HybridCacheStore {
|
||||
_RecordingHybridCacheStore()
|
||||
: super(memory: MemoryCacheStore(), persistent: PersistentCacheStore());
|
||||
|
||||
final List<String> clearedPrefixes = <String>[];
|
||||
|
||||
@override
|
||||
Future<void> clearByPrefix(String prefix) async {
|
||||
clearedPrefixes.add(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late _RecordingHybridCacheStore cacheStore;
|
||||
late SessionScopeManager manager;
|
||||
|
||||
setUp(() {
|
||||
cacheStore = _RecordingHybridCacheStore();
|
||||
manager = SessionScopeManager(cacheStore: cacheStore);
|
||||
CacheScope.resetProvider();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
CacheScope.resetProvider();
|
||||
});
|
||||
|
||||
test('activate configures stable user scope token', () async {
|
||||
await manager.activate('u-1');
|
||||
|
||||
expect(CacheScope.token(), 'user:u-1');
|
||||
expect(cacheStore.clearedPrefixes, isEmpty);
|
||||
});
|
||||
|
||||
test('activate clears previous user cache when switching user', () async {
|
||||
await manager.activate('u-1');
|
||||
await manager.activate('u-2');
|
||||
|
||||
expect(cacheStore.clearedPrefixes, ['cache:user:u-1:']);
|
||||
expect(CacheScope.token(), 'user:u-2');
|
||||
});
|
||||
|
||||
test('activate does not clear cache when user is unchanged', () async {
|
||||
await manager.activate('u-1');
|
||||
await manager.activate('u-1');
|
||||
|
||||
expect(cacheStore.clearedPrefixes, isEmpty);
|
||||
expect(CacheScope.token(), 'user:u-1');
|
||||
});
|
||||
|
||||
test('clearActiveUserScope clears current user cache', () async {
|
||||
await manager.activate('u-1');
|
||||
|
||||
await manager.clearActiveUserScope();
|
||||
|
||||
expect(cacheStore.clearedPrefixes, ['cache:user:u-1:']);
|
||||
expect(() => CacheScope.token(), throwsStateError);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/core/notification/models/reminder_alarm.dart';
|
||||
import 'package:social_app/core/notification/services/reminder_scheduler_service.dart';
|
||||
@@ -63,4 +66,125 @@ void main() {
|
||||
|
||||
expect(alarms, isEmpty);
|
||||
});
|
||||
|
||||
test('buildUpsertPlan skips unchanged pending reminders', () {
|
||||
final alarm = ReminderAlarm(
|
||||
eventId: 'evt_5',
|
||||
title: 'Sync',
|
||||
startAt: DateTime(2026, 4, 1, 10, 0),
|
||||
endAt: DateTime(2026, 4, 1, 11, 0),
|
||||
timezone: 'Asia/Shanghai',
|
||||
reminderMinutes: 15,
|
||||
fireAt: DateTime(2026, 4, 1, 9, 45),
|
||||
fireTimeBucket: 29114145,
|
||||
location: 'Room 1',
|
||||
notes: 'Bring notes',
|
||||
);
|
||||
final id = ReminderSchedulerService.notificationIdFor(
|
||||
alarm.eventId,
|
||||
alarm.fireTimeBucket,
|
||||
);
|
||||
|
||||
final pending = [
|
||||
PendingNotificationRequest(
|
||||
id,
|
||||
alarm.title,
|
||||
'开始时间 10:00 · Room 1',
|
||||
jsonEncode(alarm.toJson()),
|
||||
),
|
||||
];
|
||||
|
||||
final plan = ReminderSchedulerService.buildUpsertPlan(
|
||||
eventId: alarm.eventId,
|
||||
pending: pending,
|
||||
desired: [alarm],
|
||||
);
|
||||
|
||||
expect(plan.idsToCancel, isEmpty);
|
||||
expect(plan.alarmsToSchedule, isEmpty);
|
||||
});
|
||||
|
||||
test('buildUpsertPlan reschedules when payload changed', () {
|
||||
final desired = ReminderAlarm(
|
||||
eventId: 'evt_6',
|
||||
title: 'Daily',
|
||||
startAt: DateTime(2026, 4, 1, 8, 0),
|
||||
endAt: DateTime(2026, 4, 1, 9, 0),
|
||||
timezone: 'Asia/Shanghai',
|
||||
reminderMinutes: 10,
|
||||
fireAt: DateTime(2026, 4, 1, 7, 50),
|
||||
fireTimeBucket: 29114030,
|
||||
location: 'Desk',
|
||||
notes: 'Updated',
|
||||
);
|
||||
final stale = ReminderAlarm(
|
||||
eventId: desired.eventId,
|
||||
title: desired.title,
|
||||
startAt: desired.startAt,
|
||||
endAt: desired.endAt,
|
||||
timezone: desired.timezone,
|
||||
reminderMinutes: desired.reminderMinutes,
|
||||
fireAt: desired.fireAt,
|
||||
fireTimeBucket: desired.fireTimeBucket,
|
||||
location: desired.location,
|
||||
notes: 'Old notes',
|
||||
);
|
||||
final id = ReminderSchedulerService.notificationIdFor(
|
||||
desired.eventId,
|
||||
desired.fireTimeBucket,
|
||||
);
|
||||
|
||||
final plan = ReminderSchedulerService.buildUpsertPlan(
|
||||
eventId: desired.eventId,
|
||||
pending: [
|
||||
PendingNotificationRequest(
|
||||
id,
|
||||
stale.title,
|
||||
'开始时间 08:00 · Desk',
|
||||
jsonEncode(stale.toJson()),
|
||||
),
|
||||
],
|
||||
desired: [desired],
|
||||
);
|
||||
|
||||
expect(plan.idsToCancel, [id]);
|
||||
expect(plan.alarmsToSchedule.length, 1);
|
||||
expect(plan.alarmsToSchedule.first.notes, 'Updated');
|
||||
});
|
||||
|
||||
test('buildUpsertPlan cancels and rebuilds when payload is invalid', () {
|
||||
final desired = ReminderAlarm(
|
||||
eventId: 'evt_7',
|
||||
title: 'Check-in',
|
||||
startAt: DateTime(2026, 4, 2, 9, 0),
|
||||
endAt: DateTime(2026, 4, 2, 9, 30),
|
||||
timezone: 'Asia/Shanghai',
|
||||
reminderMinutes: 10,
|
||||
fireAt: DateTime(2026, 4, 2, 8, 50),
|
||||
fireTimeBucket: 29115530,
|
||||
location: 'Room A',
|
||||
notes: 'fresh',
|
||||
);
|
||||
final id = ReminderSchedulerService.notificationIdFor(
|
||||
desired.eventId,
|
||||
desired.fireTimeBucket,
|
||||
);
|
||||
|
||||
final plan = ReminderSchedulerService.buildUpsertPlan(
|
||||
eventId: desired.eventId,
|
||||
pending: [
|
||||
PendingNotificationRequest(
|
||||
id,
|
||||
desired.title,
|
||||
'开始时间 09:00 · Room A',
|
||||
'{"eventId":"${desired.eventId}"}',
|
||||
),
|
||||
],
|
||||
desired: [desired],
|
||||
);
|
||||
|
||||
expect(plan.idsToCancel, [id]);
|
||||
expect(plan.alarmsToSchedule.length, 1);
|
||||
expect(plan.alarmsToSchedule.first.eventId, desired.eventId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/data/cache/cache_policy.dart';
|
||||
import 'package:social_app/data/cache/cache_scope.dart';
|
||||
import 'package:social_app/data/cache/cached_repository.dart';
|
||||
import 'package:social_app/data/cache/cache_store.dart';
|
||||
|
||||
@@ -29,6 +30,14 @@ class _IntCachedRepository extends CachedRepository<int> {
|
||||
|
||||
void main() {
|
||||
group('CachedRepository', () {
|
||||
setUp(() {
|
||||
CacheScope.configureProvider(() => 'user:test');
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
CacheScope.resetProvider();
|
||||
});
|
||||
|
||||
test('reads from cache after first load', () async {
|
||||
final repo = _IntCachedRepository(
|
||||
store: HybridCacheStore(
|
||||
@@ -59,5 +68,22 @@ void main() {
|
||||
expect(refreshed, 2);
|
||||
expect(repo.loadCount, 2);
|
||||
});
|
||||
|
||||
test('falls back to remote load when scope is unavailable', () async {
|
||||
CacheScope.resetProvider();
|
||||
final repo = _IntCachedRepository(
|
||||
store: HybridCacheStore(
|
||||
memory: MemoryCacheStore(),
|
||||
persistent: PersistentCacheStore(),
|
||||
),
|
||||
);
|
||||
|
||||
final first = await repo.fetch();
|
||||
final second = await repo.fetch();
|
||||
|
||||
expect(first, 1);
|
||||
expect(second, 2);
|
||||
expect(repo.loadCount, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:social_app/data/cache/cache_scope.dart';
|
||||
import 'package:social_app/data/cache/cache_store.dart';
|
||||
|
||||
void main() {
|
||||
@@ -24,5 +25,20 @@ void main() {
|
||||
final secondRead = await hybrid.read<String>('k');
|
||||
expect(secondRead, 'v');
|
||||
});
|
||||
|
||||
test('cache invalidator no-ops without configured scope', () async {
|
||||
CacheScope.resetProvider();
|
||||
final invalidator = CacheInvalidator(
|
||||
store: HybridCacheStore(
|
||||
memory: MemoryCacheStore(),
|
||||
persistent: PersistentCacheStore(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => invalidator.invalidate('calendar:day:2026-03-31'),
|
||||
returnsNormally,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class _FakeChatApi implements ChatApi {
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
CacheScope.configureProvider(() => null);
|
||||
CacheScope.configureProvider(() => 'user:test');
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
|
||||
Reference in New Issue
Block a user