feat(social): add SavedSearchesStore with alert bookkeeping
Keystore-backed, account-scoped store mirroring SavedOffersStore. Tracks per-search seen keys (dedup, capped 500) and unread match keys (badge source), with markMatched/markViewed/newMatchCount and a changes stream.
This commit is contained in:
parent
5b1066a224
commit
530bf96358
2 changed files with 259 additions and 0 deletions
97
apps/app_seeds/test/services/saved_searches_store_test.dart
Normal file
97
apps/app_seeds/test/services/saved_searches_store_test.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:tane/services/saved_searches_store.dart';
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
void main() {
|
||||
late SavedSearchesStore store;
|
||||
|
||||
setUp(() => store = SavedSearchesStore(InMemorySecretStore()));
|
||||
tearDown(() => store.close());
|
||||
|
||||
SavedSearch search(String id, {String query = '', int createdAt = 0}) =>
|
||||
SavedSearch(id: id, label: id, query: query, createdAt: createdAt);
|
||||
|
||||
test('saves and lists newest first', () async {
|
||||
await store.save(search('a', createdAt: 100));
|
||||
await store.save(search('b', createdAt: 200));
|
||||
final list = await store.list();
|
||||
expect(list.map((s) => s.id), ['b', 'a']);
|
||||
});
|
||||
|
||||
test('save is idempotent on id', () async {
|
||||
await store.save(search('a', query: 'one'));
|
||||
await store.save(search('a', query: 'two'));
|
||||
final list = await store.list();
|
||||
expect(list, hasLength(1));
|
||||
expect(list.single.query, 'two');
|
||||
});
|
||||
|
||||
test('remove deletes a search', () async {
|
||||
await store.save(search('a'));
|
||||
await store.remove('a');
|
||||
expect(await store.list(), isEmpty);
|
||||
expect(await store.exists('a'), isFalse);
|
||||
});
|
||||
|
||||
test('markMatched reports a fresh match once, then dedups replays', () async {
|
||||
await store.save(search('a'));
|
||||
expect(await store.markMatched('a', 'author:o1'), isTrue);
|
||||
// Same key again = replay/echo, not a new alert.
|
||||
expect(await store.markMatched('a', 'author:o1'), isFalse);
|
||||
expect(await store.markMatched('a', 'author:o2'), isTrue);
|
||||
expect(await store.newMatchCount('a'), 2);
|
||||
});
|
||||
|
||||
test('markMatched returns false for an unknown search', () async {
|
||||
expect(await store.markMatched('ghost', 'author:o1'), isFalse);
|
||||
});
|
||||
|
||||
test('seeded seen keys suppress alerts for already-visible offers', () async {
|
||||
await store.save(search('a'), seedSeenKeys: ['author:o1']);
|
||||
expect(await store.markMatched('a', 'author:o1'), isFalse);
|
||||
expect(await store.newMatchCount('a'), 0);
|
||||
});
|
||||
|
||||
test('markViewed clears unread but keeps seen (no re-alert)', () async {
|
||||
await store.save(search('a'));
|
||||
await store.markMatched('a', 'author:o1');
|
||||
await store.markViewed('a');
|
||||
expect(await store.newMatchCount('a'), 0);
|
||||
// Still seen: the same offer won't alert again.
|
||||
expect(await store.markMatched('a', 'author:o1'), isFalse);
|
||||
});
|
||||
|
||||
test('totalNewMatchCount sums across searches', () async {
|
||||
await store.save(search('a'));
|
||||
await store.save(search('b'));
|
||||
await store.markMatched('a', 'author:o1');
|
||||
await store.markMatched('b', 'author:o2');
|
||||
await store.markMatched('b', 'author:o3');
|
||||
expect(await store.totalNewMatchCount(), 3);
|
||||
});
|
||||
|
||||
test('changes stream fires on mutations', () async {
|
||||
final events = <void>[];
|
||||
final sub = store.changes.listen(events.add);
|
||||
await store.save(search('a'));
|
||||
await store.markMatched('a', 'author:o1');
|
||||
await store.markViewed('a');
|
||||
await store.remove('a');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(events, hasLength(4));
|
||||
await sub.cancel();
|
||||
});
|
||||
|
||||
test('account scopes are isolated', () async {
|
||||
final secret = InMemorySecretStore();
|
||||
final s1 = SavedSearchesStore(secret, accountScope: 'acct1');
|
||||
final s2 = SavedSearchesStore(secret, accountScope: 'acct2');
|
||||
await s1.save(search('a'));
|
||||
expect(await s1.list(), hasLength(1));
|
||||
expect(await s2.list(), isEmpty);
|
||||
await s1.close();
|
||||
await s2.close();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue