import 'package:flutter_test/flutter_test.dart'; import 'package:worldhopper/models/entry.dart'; import 'package:worldhopper/models/feed.dart'; import 'package:worldhopper/models/link.dart'; import 'package:worldhopper/models/server.dart'; import 'package:worldhopper/models/stream_link.dart'; import 'package:worldhopper/services/server_software/server_software_type.dart'; void main() { group('Server', () { test('can be constructed with required fields', () { final server = Server( id: 's1', name: 'Test Server', url: 'https://example.com/opds', createdAt: DateTime(2024, 1, 1), ); expect(server.id, 's1'); expect(server.name, 'Test Server'); expect(server.url, 'https://example.com/opds'); expect(server.softwareType, isNull); }); test('requiresAuth returns true when credentials present', () { final server = Server( id: 's1', name: 'Test', url: 'https://example.com', username: 'user', password: 'pass', createdAt: DateTime(2024), ); expect(server.requiresAuth, isTrue); }); test('requiresAuth returns false when no credentials', () { final server = Server( id: 's1', name: 'Test', url: 'https://example.com', createdAt: DateTime(2024), ); expect(server.requiresAuth, isFalse); }); test('toDatabase and fromDatabase roundtrip', () { final original = Server( id: 's1', name: 'Test', url: 'https://example.com', username: 'user', password: 'pass', createdAt: DateTime(2024, 1, 1), softwareType: ServerSoftwareType.kavita, ); final row = original.toDatabase(); final restored = Server.fromDatabase(row); expect(restored.id, original.id); expect(restored.name, original.name); expect(restored.url, original.url); expect(restored.username, original.username); expect(restored.password, original.password); expect(restored.softwareType, ServerSoftwareType.kavita); }); test('JSON roundtrip preserves all fields', () { final original = Server( id: 's1', name: 'Test', url: 'https://example.com', createdAt: DateTime(2024, 1, 1), ); final json = original.toJson(); final restored = Server.fromJson(json); expect(restored, original); }); }); group('Entry', () { test('isNavigation detects subsection links', () { const entry = Entry( id: 'e1', title: 'A Collection', links: [Link(rel: 'subsection', href: '/collection/1')], ); expect(entry.isNavigation, isTrue); }); test('isNavigation is false for acquisition entries', () { const entry = Entry( id: 'e1', title: 'A Book', links: [ Link( rel: 'http://opds-spec.org/acquisition', href: '/book.epub', type: 'application/epub+zip', ), ], ); expect(entry.isNavigation, isFalse); }); test('hasStreamLink returns true when stream link present', () { const entry = Entry( id: 'e1', title: 'Comic', streamLink: StreamLink( href: 'https://example.com/page/{pageNumber}', type: 'image/jpeg', pageCount: 20, ), ); expect(entry.hasStreamLink, isTrue); expect(entry.isImageStream, isTrue); }); test('isEpub detects EPUB acquisition link', () { const entry = Entry( id: 'e1', title: 'Book', links: [ Link( rel: 'http://opds-spec.org/acquisition', href: '/book.epub', type: 'application/epub+zip', ), ], ); expect(entry.isEpub, isTrue); }); test('JSON roundtrip preserves basic fields', () { const original = Entry( id: 'e1', title: 'Test Entry', authors: ['Author One'], categories: ['Fiction'], ); final json = original.toJson(); final restored = Entry.fromJson(json); expect(restored.id, original.id); expect(restored.title, original.title); expect(restored.authors, original.authors); expect(restored.categories, original.categories); }); }); group('Feed', () { test('isNavigationFeed detects navigation entries', () { const feed = Feed( id: 'f1', title: 'Root', entries: [ Entry( id: 'e1', title: 'Library', links: [Link(rel: 'subsection', href: '/lib')], ), ], ); expect(feed.isNavigationFeed, isTrue); expect(feed.isAcquisitionFeed, isFalse); }); test('isAcquisitionFeed when no navigation entries', () { const feed = Feed( id: 'f1', title: 'Books', entries: [ Entry( id: 'e1', title: 'Book 1', links: [ Link( rel: 'http://opds-spec.org/acquisition', href: '/book.epub', ), ], ), ], ); expect(feed.isAcquisitionFeed, isTrue); expect(feed.isNavigationFeed, isFalse); }); test('nextLink returns pagination link', () { const feed = Feed( id: 'f1', title: 'Books', links: [ Link(rel: 'next', href: '/page/2'), Link(rel: 'self', href: '/page/1'), ], ); expect(feed.nextLink, isNotNull); expect(feed.nextLink!.href, '/page/2'); }); test('nextLink returns null when not present', () { const feed = Feed( id: 'f1', title: 'Books', links: [Link(rel: 'self', href: '/page/1')], ); expect(feed.nextLink, isNull); }); }); group('StreamLink', () { test('getPageUrl replaces pageNumber placeholder', () { const link = StreamLink( href: 'https://example.com/page/{pageNumber}', type: 'image/jpeg', pageCount: 50, ); expect(link.getPageUrl(5), 'https://example.com/page/5'); }); test('getPageUrl replaces both pageNumber and maxWidth', () { const link = StreamLink( href: 'https://example.com/page/{pageNumber}?maxWidth={maxWidth}', type: 'image/jpeg', pageCount: 50, ); expect( link.getPageUrl(3, maxWidth: 1200), 'https://example.com/page/3?maxWidth=1200', ); }); test('supportsMaxWidth detects maxWidth placeholder', () { const withMax = StreamLink( href: 'https://example.com/{pageNumber}?maxWidth={maxWidth}', type: 'image/jpeg', pageCount: 10, ); const withoutMax = StreamLink( href: 'https://example.com/{pageNumber}', type: 'image/jpeg', pageCount: 10, ); expect(withMax.supportsMaxWidth, isTrue); expect(withoutMax.supportsMaxWidth, isFalse); }); }); group('Link', () { test('JSON roundtrip', () { const original = Link( rel: 'acquisition', href: '/book.epub', type: 'application/epub+zip', title: 'Download', ); final json = original.toJson(); final restored = Link.fromJson(json); expect(restored, original); }); }); }