feat: store subscription posts with a read mark and split the library controller
This commit is contained in:
parent
b311b9f7dd
commit
953ae798eb
15 changed files with 1787 additions and 386 deletions
139
app/test/database_test.dart
Normal file
139
app/test/database_test.dart
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'package:mews/database/database.dart';
|
||||
import 'package:mews/mews/document.dart';
|
||||
|
||||
MewsEntry _entry(String url, {String title = 'A post', int day = 8}) =>
|
||||
MewsEntry(url: url, date: DateTime(2026, 10, day), title: title);
|
||||
|
||||
void main() {
|
||||
late MewsDatabase db;
|
||||
|
||||
setUp(() {
|
||||
db = MewsDatabase.test(NativeDatabase.memory());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('a page can only be bookmarked once', () async {
|
||||
await db.addBookmark('https://example.com/', 'Example');
|
||||
await db.addBookmark('https://example.com/', 'Example again');
|
||||
expect(await db.allBookmarks(), hasLength(1));
|
||||
expect(await db.countBookmarks(), 1);
|
||||
});
|
||||
|
||||
test('a site can only be subscribed to once', () async {
|
||||
await db.addSubscription('https://example.com/', 'Example', null);
|
||||
await db.addSubscription('https://example.com/', 'Example', null);
|
||||
expect(await db.allSubscriptions(), hasLength(1));
|
||||
expect(await db.countSubscriptions(), 1);
|
||||
});
|
||||
|
||||
test('finding a post again keeps its read mark', () async {
|
||||
const site = 'https://example.com/';
|
||||
await db.addSubscription(site, 'Example', null);
|
||||
await db.addEntries(site, [_entry('https://example.com/a.html')]);
|
||||
expect(await db.unreadCount(), 1);
|
||||
|
||||
await db.markEntryRead('https://example.com/a.html');
|
||||
expect(await db.unreadCount(), 0);
|
||||
|
||||
// A refresh sees the same post again; it must not come back unread.
|
||||
final fresh = await db.addEntries(site, [_entry('https://example.com/a.html')]);
|
||||
expect(fresh, isEmpty);
|
||||
expect(await db.unreadCount(), 0);
|
||||
});
|
||||
|
||||
test('unread counts split by site', () async {
|
||||
await db.addEntries('https://a.example/', [
|
||||
_entry('https://a.example/one.html'),
|
||||
_entry('https://a.example/two.html', day: 9),
|
||||
]);
|
||||
await db.addEntries('https://b.example/', [_entry('https://b.example/one.html')]);
|
||||
|
||||
expect(await db.unreadCount(), 3);
|
||||
expect(await db.unreadCount(siteUrl: 'https://a.example/'), 2);
|
||||
expect(await db.unreadCounts(),
|
||||
{'https://a.example/': 2, 'https://b.example/': 1});
|
||||
});
|
||||
|
||||
test('describing a site leaves the fields it is not given', () async {
|
||||
const site = 'https://example.com/';
|
||||
await db.addSubscription(site, 'Signal Bars', null);
|
||||
|
||||
// The old single update method blanked the title whenever a refresh only
|
||||
// had a feed address to record.
|
||||
await db.describeSubscription(site, atomHref: 'https://example.com/feed.xml');
|
||||
final row = (await db.allSubscriptions()).single;
|
||||
expect(row.title, 'Signal Bars');
|
||||
expect(row.atomHref, 'https://example.com/feed.xml');
|
||||
});
|
||||
|
||||
test('checking a site records the time without touching its posts', () async {
|
||||
const site = 'https://example.com/';
|
||||
await db.addSubscription(site, 'Example', null);
|
||||
await db.addEntries(site, [_entry('https://example.com/a.html')]);
|
||||
|
||||
final checkedAt = DateTime(2026, 10, 11, 9, 30);
|
||||
await db.touchSubscription(site, checkedAt);
|
||||
expect((await db.allSubscriptions()).single.lastCheckedAt, checkedAt);
|
||||
expect(await db.unreadCount(), 1);
|
||||
});
|
||||
|
||||
test('history counts addresses rather than visits', () async {
|
||||
await db.recordVisit('https://example.com/a.html', 'A');
|
||||
await db.recordVisit('https://example.com/a.html', 'A');
|
||||
await db.recordVisit('https://example.com/b.html', 'B');
|
||||
expect(await db.countHistory(), 2);
|
||||
});
|
||||
|
||||
test('an older database is recreated rather than migrated', () async {
|
||||
final dir = Directory.systemTemp.createTempSync('mews-schema');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final file = File(p.join(dir.path, 'mews.sqlite'));
|
||||
|
||||
final before = MewsDatabase.test(NativeDatabase(file));
|
||||
await before.addBookmark('https://example.com/', 'Example');
|
||||
await before.addEntries(
|
||||
'https://example.com/', [_entry('https://example.com/a.html')]);
|
||||
// Make the file look like the shipped version 1: the posts and tags
|
||||
// tables did not exist, and the version counter said 1.
|
||||
await before.customStatement('DROP TABLE feed_entries');
|
||||
await before.customStatement('DROP TABLE item_tags');
|
||||
await before.customStatement('PRAGMA user_version = 1');
|
||||
await before.close();
|
||||
|
||||
// Opening it now runs the destructive upgrade: the reader loses their
|
||||
// local data, which is the agreed trade before release, but the app must
|
||||
// come up rather than fail on a table that was never there.
|
||||
final after = MewsDatabase.test(NativeDatabase(file));
|
||||
addTearDown(after.close);
|
||||
expect(await after.allBookmarks(), isEmpty);
|
||||
expect(await after.unreadCount(), 0);
|
||||
|
||||
// The new schema works afterwards.
|
||||
await after.addBookmark('https://example.com/', 'Example');
|
||||
await after.addEntries(
|
||||
'https://example.com/', [_entry('https://example.com/a.html')]);
|
||||
expect(await after.countBookmarks(), 1);
|
||||
expect(await after.unreadCount(), 1);
|
||||
});
|
||||
|
||||
test('tags follow the item they are on', () async {
|
||||
const url = 'https://example.com/';
|
||||
await db.addBookmark(url, 'Example');
|
||||
await db.addTag(TagKind.bookmark, url, 'phones');
|
||||
await db.addTag(TagKind.bookmark, url, 'phones');
|
||||
expect(await db.tagsOf(TagKind.bookmark, url), ['phones']);
|
||||
|
||||
// Removing the bookmark takes its tags with it.
|
||||
await db.removeBookmark(url);
|
||||
expect(await db.tagsOf(TagKind.bookmark, url), isEmpty);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue