worldhopper/test/providers/series_navigation_provider_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

127 lines
3.3 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:worldhopper/models/entry.dart';
import 'package:worldhopper/models/feed.dart';
import 'package:worldhopper/providers/series_navigation_provider.dart';
Entry _entry(String id, String title) => Entry(id: id, title: title);
Feed _feedWith(List<Entry> entries) =>
Feed(id: 'feed-1', title: 'Series', entries: entries);
/// Extracts the next-in-series logic from the provider for unit testing.
/// This mirrors the logic in nextInSeriesProvider without needing Riverpod.
Entry? _findNextInSeries(Feed feed, String currentEntryId) {
final entries = feed.entries;
for (var i = 0; i < entries.length; i++) {
if (entries[i].id == currentEntryId) {
if (i + 1 < entries.length) {
return entries[i + 1];
}
return null;
}
}
return null;
}
void main() {
group('nextInSeriesProvider', () {
test('returns next entry when current is in the middle', () {
final feed = _feedWith([
_entry('ch1', 'Chapter 1'),
_entry('ch2', 'Chapter 2'),
_entry('ch3', 'Chapter 3'),
]);
final result = _findNextInSeries(feed, 'ch1');
expect(result, isNotNull);
expect(result!.id, 'ch2');
expect(result.title, 'Chapter 2');
});
test('returns next entry when current is second-to-last', () {
final feed = _feedWith([
_entry('ch1', 'Chapter 1'),
_entry('ch2', 'Chapter 2'),
_entry('ch3', 'Chapter 3'),
]);
final result = _findNextInSeries(feed, 'ch2');
expect(result, isNotNull);
expect(result!.id, 'ch3');
});
test('returns null when current entry is the last', () {
final feed = _feedWith([
_entry('ch1', 'Chapter 1'),
_entry('ch2', 'Chapter 2'),
_entry('ch3', 'Chapter 3'),
]);
final result = _findNextInSeries(feed, 'ch3');
expect(result, isNull);
});
test('returns null when current entry is not found in feed', () {
final feed = _feedWith([
_entry('ch1', 'Chapter 1'),
_entry('ch2', 'Chapter 2'),
]);
final result = _findNextInSeries(feed, 'unknown-id');
expect(result, isNull);
});
test('returns null for an empty feed', () {
final feed = _feedWith([]);
final result = _findNextInSeries(feed, 'ch1');
expect(result, isNull);
});
test('returns null for single-entry feed', () {
final feed = _feedWith([_entry('ch1', 'Only Chapter')]);
final result = _findNextInSeries(feed, 'ch1');
expect(result, isNull);
});
});
group('NextInSeriesRequest equality', () {
test('equal requests have same hashCode', () {
const a = NextInSeriesRequest(
serverId: 's1',
feedUrl: '/feed',
currentEntryId: 'ch1',
);
const b = NextInSeriesRequest(
serverId: 's1',
feedUrl: '/feed',
currentEntryId: 'ch1',
);
expect(a, equals(b));
expect(a.hashCode, equals(b.hashCode));
});
test('different requests are not equal', () {
const a = NextInSeriesRequest(
serverId: 's1',
feedUrl: '/feed',
currentEntryId: 'ch1',
);
const b = NextInSeriesRequest(
serverId: 's1',
feedUrl: '/feed',
currentEntryId: 'ch2',
);
expect(a, isNot(equals(b)));
});
});
}