Files
BlockSeasons/test/data/remote/content_downloader_test.dart
T

180 lines
5.4 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:block_seasons/data/remote/content_downloader.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
void main() {
late Directory tmp;
setUp(() async {
tmp = await Directory.systemTemp.createTemp('bs_cache_');
});
tearDown(() async {
if (await tmp.exists()) await tmp.delete(recursive: true);
});
String shaOf(String body) => sha256.convert(utf8.encode(body)).toString();
// Minimal pack body (repository parses later; downloader only verifies
// bytes).
const packBody = '{"schemaVersion":1,"seasonId":"season_002"}';
Map<String, dynamic> manifestJson({String? sha, int version = 1}) => {
'schemaVersion': 1,
'minAppBuild': 1,
'current': 'season_002',
'seasons': [
{
'seasonId': 'season_002',
'version': version,
'packUrl': 'seasons/season_002/pack.json',
'sha256': sha ?? shaOf(packBody),
},
],
};
MockClient okClient({String? sha, int version = 1}) =>
MockClient((request) async {
if (request.url.path.endsWith('manifest.json')) {
return http.Response(
jsonEncode(manifestJson(sha: sha, version: version)),
200,
headers: {'content-type': 'application/json'},
);
}
if (request.url.path.endsWith('pack.json')) {
return http.Response(packBody, 200);
}
return http.Response('not found', 404);
});
test('downloads, verifies, and caches a new season', () async {
final downloader = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: okClient(),
);
final updated = await downloader.sync();
expect(updated, isTrue);
final cached = File('${tmp.path}/seasons/season_002/pack.json');
expect(await cached.readAsString(), packBody);
expect(downloader.cachedVersion('season_002'), 1);
});
test('second sync with same version is a no-op', () async {
final downloader = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: okClient(),
);
await downloader.sync();
expect(await downloader.sync(), isFalse);
});
test('checksum mismatch rejects the pack and keeps cache clean', () async {
final downloader = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: okClient(sha: 'deadbeef'),
);
final updated = await downloader.sync();
expect(updated, isFalse);
expect(
File('${tmp.path}/seasons/season_002/pack.json').existsSync(),
isFalse,
);
// No stray temp files left behind.
final leftovers = tmp
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('.tmp'));
expect(leftovers, isEmpty);
});
test('network failure leaves existing cache untouched', () async {
final good = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: okClient(),
);
await good.sync();
final offline = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: MockClient((_) async => throw const SocketException('off')),
);
expect(await offline.sync(), isFalse);
expect(
File('${tmp.path}/seasons/season_002/pack.json').existsSync(),
isTrue,
);
});
test('version bump re-downloads', () async {
await ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: okClient(version: 1),
).sync();
final updated = await ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: okClient(version: 2),
).sync();
expect(updated, isTrue);
});
test('malicious seasonId never escapes the cache dir', () async {
final outside = Directory('${tmp.path}/../bs_escape_probe');
addTearDown(() async {
if (await outside.exists()) await outside.delete(recursive: true);
});
final client = MockClient((request) async {
if (request.url.path.endsWith('manifest.json')) {
return http.Response(
jsonEncode({
'schemaVersion': 1,
'minAppBuild': 1,
'current': 'x',
'seasons': [
{
'seasonId': '../bs_escape_probe',
'version': 1,
'packUrl': 'seasons/x/pack.json',
'sha256': shaOf(packBody),
},
],
}),
200,
);
}
return http.Response(packBody, 200);
});
final downloader = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: client,
);
expect(await downloader.sync(), isFalse);
expect(await outside.exists(), isFalse);
});
test('corrupt cache index is treated as empty and re-downloads', () async {
await File('${tmp.path}/cache_index.json').create(recursive: true);
await File('${tmp.path}/cache_index.json').writeAsString('{broken');
final downloader = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: okClient(),
);
expect(await downloader.sync(), isTrue);
expect(downloader.cachedVersion('season_002'), 1);
});
}