feat: 重构 Reminder Notification 系统并更新应用包名
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/core/inbox/inbox_sync_store.dart';
|
||||
import 'package:social_app/data/network/i_api_client.dart';
|
||||
import 'package:social_app/features/messages/data/apis/inbox_api.dart'
|
||||
show InboxApi;
|
||||
import 'package:social_app/features/messages/data/models/inbox_message.dart';
|
||||
import 'package:social_app/features/messages/data/repositories/inbox_repository.dart';
|
||||
|
||||
class _FakeInboxRepository implements InboxRepository {
|
||||
List<InboxMessage> unread = <InboxMessage>[];
|
||||
List<InboxMessage> read = <InboxMessage>[];
|
||||
|
||||
@override
|
||||
Future<List<InboxMessage>> getMessages({
|
||||
bool? isRead,
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
if (isRead == true) {
|
||||
return read;
|
||||
}
|
||||
if (isRead == false) {
|
||||
return unread;
|
||||
}
|
||||
return <InboxMessage>[...unread, ...read];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InboxMessage> markAsRead(String messageId) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeApiClient implements IApiClient {
|
||||
final StreamController<String> streamController =
|
||||
StreamController<String>.broadcast();
|
||||
|
||||
@override
|
||||
Future<Stream<String>> getSseLines(
|
||||
String path, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
return streamController.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> delete<T>(String path, {data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, String>? queryParameters,
|
||||
Options? options,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> patch<T>(String path, {data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> post<T>(String path, {data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> put<T>(String path, {data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
InboxMessage _message({
|
||||
required String id,
|
||||
required bool isRead,
|
||||
InboxMessageStatus status = InboxMessageStatus.pending,
|
||||
}) {
|
||||
return InboxMessage(
|
||||
id: id,
|
||||
recipientId: 'u1',
|
||||
senderId: 'u2',
|
||||
messageType: InboxMessageType.calendar,
|
||||
scheduleItemId: 's1',
|
||||
friendshipId: null,
|
||||
content: const {'type': 'invite'},
|
||||
isRead: isRead,
|
||||
status: status,
|
||||
createdAt: DateTime.parse('2026-03-30T07:00:00Z'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _emitEnvelope(
|
||||
_FakeApiClient api,
|
||||
Map<String, Object?> envelope, {
|
||||
String eventType = 'INBOX_MESSAGE_CREATED',
|
||||
String streamId = '1743313300000-0',
|
||||
}) async {
|
||||
api.streamController.add('id: $streamId');
|
||||
api.streamController.add('event: $eventType');
|
||||
api.streamController.add('data: ${jsonEncode(envelope)}');
|
||||
api.streamController.add('');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 30));
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('InboxSyncStore increments unread count on created event', () async {
|
||||
final repo = _FakeInboxRepository();
|
||||
final apiClient = _FakeApiClient();
|
||||
final store = InboxSyncStore(
|
||||
repository: repo,
|
||||
inboxApi: InboxApi(apiClient),
|
||||
);
|
||||
addTearDown(() async {
|
||||
await store.stop();
|
||||
await apiClient.streamController.close();
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
await store.resetForUser('u1');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 60));
|
||||
|
||||
await _emitEnvelope(apiClient, {
|
||||
'event_id': 'e1',
|
||||
'occurred_at': '2026-03-30T07:00:00Z',
|
||||
'user_id': 'u1',
|
||||
'message_id': 'm1',
|
||||
'op': 'created',
|
||||
'version': 1774854000000,
|
||||
'data': {
|
||||
'message': {
|
||||
'id': 'm1',
|
||||
'recipient_id': 'u1',
|
||||
'sender_id': 'u2',
|
||||
'message_type': 'calendar',
|
||||
'schedule_item_id': 's1',
|
||||
'friendship_id': null,
|
||||
'content': {'type': 'invite'},
|
||||
'is_read': false,
|
||||
'status': 'pending',
|
||||
'created_at': '2026-03-30T07:00:00Z',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(store.unreadCount, 1);
|
||||
expect(store.unreadMessages.single.id, 'm1');
|
||||
});
|
||||
|
||||
test(
|
||||
'InboxSyncStore decrements unread count on read_changed event',
|
||||
() async {
|
||||
final repo = _FakeInboxRepository()
|
||||
..unread = <InboxMessage>[_message(id: 'm1', isRead: false)];
|
||||
final apiClient = _FakeApiClient();
|
||||
final store = InboxSyncStore(
|
||||
repository: repo,
|
||||
inboxApi: InboxApi(apiClient),
|
||||
);
|
||||
addTearDown(() async {
|
||||
await store.stop();
|
||||
await apiClient.streamController.close();
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
await store.resetForUser('u1');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 60));
|
||||
expect(store.unreadCount, 1);
|
||||
|
||||
await _emitEnvelope(
|
||||
apiClient,
|
||||
{
|
||||
'event_id': 'e2',
|
||||
'occurred_at': '2026-03-30T07:00:01Z',
|
||||
'user_id': 'u1',
|
||||
'message_id': 'm1',
|
||||
'op': 'read_changed',
|
||||
'version': 1774854001000,
|
||||
'data': {'is_read': true},
|
||||
},
|
||||
eventType: 'INBOX_MESSAGE_READ_CHANGED',
|
||||
streamId: '1743313301000-0',
|
||||
);
|
||||
|
||||
expect(store.unreadCount, 0);
|
||||
expect(store.readMessages.single.id, 'm1');
|
||||
},
|
||||
);
|
||||
|
||||
test('InboxSyncStore ignores stale version events', () async {
|
||||
final repo = _FakeInboxRepository()
|
||||
..unread = <InboxMessage>[_message(id: 'm1', isRead: false)];
|
||||
final apiClient = _FakeApiClient();
|
||||
final store = InboxSyncStore(
|
||||
repository: repo,
|
||||
inboxApi: InboxApi(apiClient),
|
||||
);
|
||||
addTearDown(() async {
|
||||
await store.stop();
|
||||
await apiClient.streamController.close();
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
await store.resetForUser('u1');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 60));
|
||||
|
||||
await _emitEnvelope(
|
||||
apiClient,
|
||||
{
|
||||
'event_id': 'e3',
|
||||
'occurred_at': '2026-03-30T07:00:00Z',
|
||||
'user_id': 'u1',
|
||||
'message_id': 'm1',
|
||||
'op': 'read_changed',
|
||||
'version': 1774853900000,
|
||||
'data': {'is_read': true},
|
||||
},
|
||||
eventType: 'INBOX_MESSAGE_READ_CHANGED',
|
||||
streamId: '1743313200000-0',
|
||||
);
|
||||
|
||||
expect(store.unreadCount, 1);
|
||||
});
|
||||
|
||||
test('InboxSyncStore clears stale state on user switch', () async {
|
||||
final repo = _FakeInboxRepository()
|
||||
..unread = <InboxMessage>[_message(id: 'm1', isRead: false)];
|
||||
final apiClient = _FakeApiClient();
|
||||
final store = InboxSyncStore(
|
||||
repository: repo,
|
||||
inboxApi: InboxApi(apiClient),
|
||||
);
|
||||
addTearDown(() async {
|
||||
await store.stop();
|
||||
await apiClient.streamController.close();
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
await store.resetForUser('u1');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 60));
|
||||
expect(store.unreadCount, 1);
|
||||
|
||||
repo.unread = <InboxMessage>[];
|
||||
await store.resetForUser('u2');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 60));
|
||||
|
||||
expect(store.unreadCount, 0);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user