[verified] Publish ReelBox source under GPLv3
This commit is contained in:
commit
db2b505ca3
445 files changed
+105274
No files matched your search
@@ -0,0 +1,218 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user