worldhopper/test/services/server_software/opds_server_software_test.dart
Felipe M. 58a562abe8
feat: refactor server software abstraction with Kavita REST API integration
Rename all OPDS-prefixed models to generic names (Server, Feed, Entry,
Link, StreamLink), expand the ServerSoftware interface to cover all
server interactions, and implement a full Kavita REST API client that
replaces the OPDS delegation.

- Rename OPDS* models to generic names across ~60 files
- Add database migrations 13 (opds_id → entry_id) and 14 (unify credentials)
- Create OPDSServerSoftware wrapping existing OPDS services
- Create KavitaApiClient for direct Kavita REST API calls
- Create KavitaFeedMapper to convert Kavita JSON to Feed/Entry models
- Rewrite KavitaServerSoftware to use native API (no OPDS delegation)
- Unify server credentials (remove softwareUsername/softwarePassword)
- Simplify add/edit server UI to single auth section
- Add test connection button to server add/edit screens
- Add progress indicator to PublicationCard using local and server data
- Eliminate softwareType branching in reader screens
- Add EntryProgress and fetchEntryProgress to ServerSoftware interface
- Fix Entry.acquisitionLink crash on empty links
- Add 59 new tests covering models, services, and providers
2026-04-06 17:46:48 +02:00

122 lines
3.8 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:worldhopper/models/entry.dart';
import 'package:worldhopper/models/stream_link.dart';
import 'package:worldhopper/services/server_software/server_software.dart';
// Test the fetchEntryProgress logic directly since it's a pure data extraction
// from StreamLink.lastRead — no HTTP or platform channels needed.
Map<String, EntryProgress> _extractProgress(List<Entry> entries) {
final result = <String, EntryProgress>{};
for (final entry in entries) {
if (entry.hasStreamLink &&
entry.streamLink!.lastRead != null &&
entry.streamLink!.lastRead! > 0) {
result[entry.id] = EntryProgress(
entryId: entry.id,
currentPage: entry.streamLink!.lastRead!,
totalPages: entry.streamLink!.pageCount,
);
}
}
return result;
}
StreamLink _streamLink({int pageCount = 50, int? lastRead}) => StreamLink(
href: 'https://example.com/page/{pageNumber}',
type: 'image/jpeg',
pageCount: pageCount,
lastRead: lastRead,
);
Entry _entry(String id, {StreamLink? streamLink}) => Entry(
id: id,
title: 'Entry $id',
streamLink: streamLink,
);
void main() {
group('OPDS fetchEntryProgress logic', () {
test('extracts progress from StreamLink.lastRead', () {
final entries = [
_entry('e1', streamLink: _streamLink(pageCount: 100, lastRead: 49)),
_entry('e2', streamLink: _streamLink(pageCount: 50, lastRead: 24)),
];
final result = _extractProgress(entries);
expect(result, hasLength(2));
expect(result['e1']!.currentPage, 49);
expect(result['e1']!.totalPages, 100);
expect(result['e1']!.percentage, 0.5);
expect(result['e2']!.currentPage, 24);
expect(result['e2']!.totalPages, 50);
expect(result['e2']!.percentage, 0.5);
});
test('skips entries without StreamLink', () {
final entries = [
_entry('e1'),
_entry('e2', streamLink: _streamLink(pageCount: 10, lastRead: 5)),
];
final result = _extractProgress(entries);
expect(result, hasLength(1));
expect(result.containsKey('e1'), isFalse);
expect(result.containsKey('e2'), isTrue);
});
test('skips entries with lastRead = null', () {
final entries = [
_entry('e1', streamLink: _streamLink(pageCount: 10, lastRead: null)),
];
final result = _extractProgress(entries);
expect(result, isEmpty);
});
test('skips entries with lastRead = 0', () {
final entries = [
_entry('e1', streamLink: _streamLink(pageCount: 10, lastRead: 0)),
];
final result = _extractProgress(entries);
expect(result, isEmpty);
});
test('returns empty map for empty entry list', () {
final result = _extractProgress([]);
expect(result, isEmpty);
});
test('marks completed entry correctly', () {
final entries = [
_entry('e1', streamLink: _streamLink(pageCount: 10, lastRead: 9)),
];
final result = _extractProgress(entries);
expect(result['e1']!.isCompleted, isTrue);
expect(result['e1']!.percentage, 1.0);
});
test('handles mixed entries with and without progress', () {
final entries = [
_entry('e1', streamLink: _streamLink(pageCount: 20, lastRead: 10)),
_entry('e2'),
_entry('e3', streamLink: _streamLink(pageCount: 30, lastRead: null)),
_entry('e4', streamLink: _streamLink(pageCount: 40, lastRead: 0)),
_entry('e5', streamLink: _streamLink(pageCount: 50, lastRead: 25)),
];
final result = _extractProgress(entries);
expect(result, hasLength(2));
expect(result.containsKey('e1'), isTrue);
expect(result.containsKey('e5'), isTrue);
expect(result['e1']!.percentage, closeTo(0.55, 0.01));
expect(result['e5']!.percentage, closeTo(0.52, 0.01));
});
});
}