Files
reelbox/test/api_selector_test.dart

219 lines
7.2 KiB
Dart

import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:reelbox/data/api_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'api_service_test.dart' as fixtures;
const iranBase = 'https://reelbox.dartty.top/api/';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
test(
'malicious persisted selections cannot redirect the first network request',
() async {
for (final invalid in [
'https://reelbox.dartty.top:8443/api/',
'http://reelbox.dartty.top:8443/api/',
'https://reelbox.dartty.top.evil.example:8443/api/',
'http://127.0.0.1:8123/api/',
'https://user@reelbox.dartty.top:8443/api/',
]) {
SharedPreferences.setMockInitialValues({
'reelbox.api-selection.v1': invalid,
});
final adapter = fixtures.StubAdapter((_) => fixtures.jsonBody([]));
final api = ApiService(dio: Dio()..httpClientAdapter = adapter);
await api.genres();
expect(api.baseUrl, ApiService.defaultBaseUrl, reason: invalid);
expect(
adapter.requests.single.uri.origin,
'https://reelbox.dartty.top',
);
}
},
);
test('explicit choice wins over delayed cache restoration', () async {
final cacheStarted = Completer<void>();
final cache = Completer<SharedPreferences>();
var reads = 0;
final api = ApiService(
preferences: () {
if (reads++ == 0) {
cacheStarted.complete();
return cache.future;
}
return SharedPreferences.getInstance();
},
);
final restoration = api.initialize(discover: false);
await cacheStarted.future;
await api.setBaseUrl(iranBase);
cache.complete(await SharedPreferences.getInstance());
await restoration;
expect(api.baseUrl, iranBase);
});
test('actor search restores the selected origin before building its absolute route', () async {
await ApiService().setBaseUrl(iranBase);
final adapter = fixtures.StubAdapter((_) => fixtures.jsonBody([]));
final restarted = ApiService(dio: Dio()..httpClientAdapter = adapter);
await restarted.actors(search: 'Example');
expect(adapter.requests.single.uri.origin, 'https://reelbox.dartty.top');
});
test('Iran catalogue preserves media URLs and never falls back on failure', () async {
const media =
'https://original-media.example:9443/video.mp4?token=Exact%2FValue&x=1';
final adapter = fixtures.StubAdapter(
(_) => fixtures.jsonBody({
'id': 7,
'title': 'Movie',
'sources': [
{'id': 3, 'url': media, 'type': 'mp4'},
],
}),
);
final api = ApiService(
baseUrl: iranBase,
dio: Dio()..httpClientAdapter = adapter,
);
expect((await api.poster(7)).sources.single.url, media);
final offline = fixtures.StubAdapter(
(r) => throw DioException(
requestOptions: r,
type: DioExceptionType.connectionError,
),
);
final failing = ApiService(
baseUrl: iranBase,
dio: Dio()..httpClientAdapter = offline,
);
await expectLater(failing.genres(), throwsA(isA<ApiException>()));
expect(failing.baseUrl, iranBase);
expect(offline.requests, hasLength(2));
expect(
offline.requests.every(
(r) => r.uri.origin == 'https://reelbox.dartty.top',
),
isTrue,
);
});
test(
'concurrent initialization shares migration without discovery or probes',
() async {
SharedPreferences.setMockInitialValues({
'reelbox.api-selection.v1': 'https://ninwinios.info/api/',
});
final cache = Completer<SharedPreferences>();
var reads = 0;
final adapter = fixtures.StubAdapter((_) => fixtures.jsonBody([]));
final api = ApiService(
dio: Dio()..httpClientAdapter = adapter,
preferences: () {
reads++;
return cache.future;
},
);
final initialization = Future.wait([api.initialize(), api.initialize()]);
cache.complete(await SharedPreferences.getInstance());
await initialization;
expect(reads, 1);
expect(adapter.requests, isEmpty);
expect(api.baseUrl, iranBase);
final restarted = ApiService();
await restarted.initialize(discover: false);
expect(restarted.baseUrl, iranBase);
},
);
test(
'unsigned advertisements are never fetched or used during initialization',
() async {
for (final advertised in [iranBase, 'http://ninwinios.info/api/']) {
final adapter = fixtures.StubAdapter(
(r) => r.uri.host == 'ninnies-server.info'
? ResponseBody.fromString(advertised, 200)
: fixtures.jsonBody([
{'id': 1, 'title': 'Drama'},
]),
);
final api = ApiService(dio: Dio()..httpClientAdapter = adapter);
await api.initialize();
expect(api.baseUrl, ApiService.defaultBaseUrl);
expect(adapter.requests, isEmpty);
}
},
);
test('only exact HTTPS service origins and constructor loopback fixtures are accepted', () async {
for (final invalid in [
'http://reelbox.dartty.top:8443/api/',
'https://reelbox.dartty.top:8443/api/',
'https://reelbox.dartty.top:8444/api/',
'https://reelbox.dartty.top.evil.example:8443/api/',
'https://evil.reelbox.dartty.top:8443/api/',
'https://ninwinios.info:8443/api/',
'https://ninwinios.info/api/',
'https://user@reelbox.dartty.top:8443/api/',
'https://reelbox.dartty.top/api/?q=1',
'https://reelbox.dartty.top/api/#fragment',
'https://reelbox.dartty.top/api/../api/',
' $iranBase',
]) {
expect(
() => ApiService(baseUrl: invalid),
throwsA(isA<ApiException>()),
reason: invalid,
);
await expectLater(
ApiService().setBaseUrl(invalid),
throwsA(isA<ApiException>()),
reason: invalid,
);
}
for (final base in [
'http://127.0.0.1:4589/api/',
'http://localhost:4589/api/',
'http://[::1]:4589/api/',
]) {
final adapter = fixtures.StubAdapter((_) => fixtures.jsonBody([]));
final api = ApiService(
baseUrl: base,
dio: Dio()..httpClientAdapter = adapter,
);
await api.genres();
expect(adapter.requests.single.uri.origin, Uri.parse(base).origin);
}
});
test(
'explicit Iran choice persists and restores before first catalogue request',
() async {
final first = ApiService();
await first.setBaseUrl(iranBase);
final adapter = fixtures.StubAdapter((_) => fixtures.jsonBody([]));
final restarted = ApiService(dio: Dio()..httpClientAdapter = adapter);
await restarted.genres();
expect(restarted.baseUrl, iranBase);
expect(adapter.requests.single.uri.origin, 'https://reelbox.dartty.top');
await restarted.initialize();
expect(
adapter.requests,
hasLength(1),
reason: 'Iran must not run original discovery',
);
await restarted.setBaseUrl(ApiService.defaultBaseUrl);
final fresh = ApiService();
await fresh.initialize(discover: false);
expect(fresh.baseUrl, iranBase);
},
);
}