cyberhybridhub/server/test/spelling/spelling_audio_handler_test.dart
nathanAndrinoid a521a74214
Some checks failed
Admin portal tests / test (push) Has been cancelled
balance controled working spelling and market questions.
2026-06-17 04:20:45 +00:00

210 lines
7.5 KiB
Dart

import 'dart:io';
import 'package:cyberhybridhub_server/handlers/spelling_audio_handler.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:shelf/shelf.dart';
import 'package:test/test.dart';
void main() {
group('SpellingAudioHandler tests', () {
late Directory tempCacheDir;
late File logFile;
setUp(() {
tempCacheDir = Directory('spelling_audio_cache');
if (tempCacheDir.existsSync()) {
try {
tempCacheDir.deleteSync(recursive: true);
} catch (_) {}
}
logFile = File('spelling_audio_errors.log');
if (logFile.existsSync()) {
try {
logFile.deleteSync();
} catch (_) {}
}
});
tearDown(() {
if (tempCacheDir.existsSync()) {
try {
tempCacheDir.deleteSync(recursive: true);
} catch (_) {}
}
if (logFile.existsSync()) {
try {
logFile.deleteSync();
} catch (_) {}
}
});
test('serves cached file if exists', () async {
tempCacheDir.createSync(recursive: true);
final File localFile = File('${tempCacheDir.path}/hello.mp3');
localFile.writeAsBytesSync([1, 2, 3, 4]);
final handler = spellingAudioHandler();
final request = Request('GET', Uri.parse('http://localhost/v1/spelling/audio/hello.mp3'));
final response = await handler(request);
expect(response.statusCode, 200);
expect(response.headers['Content-Type'], 'audio/mpeg');
expect(await response.read().first, [1, 2, 3, 4]);
});
test('downloads, caches and serves file on-demand if not cached', () async {
final mockClient = MockClient((request) async {
if (request.url.path.contains('/entries/en/hello')) {
const jsonResponse = '''
[
{
"word": "hello",
"phonetics": [
{
"audio": "https://example.com/hello.mp3"
}
]
}
]
''';
return http.Response(jsonResponse, 200);
}
if (request.url.toString() == 'https://example.com/hello.mp3') {
return http.Response.bytes([5, 6, 7, 8], 200);
}
return http.Response('Not found', 404);
});
final handler = spellingAudioHandler(httpClient: mockClient);
final request = Request('GET', Uri.parse('http://localhost/v1/spelling/audio/hello.mp3'));
final response = await handler(request);
expect(response.statusCode, 200);
expect(response.headers['Content-Type'], 'audio/mpeg');
expect(await response.read().first, [5, 6, 7, 8]);
// Check if file is saved locally
final File localFile = File('${tempCacheDir.path}/hello.mp3');
expect(localFile.existsSync(), isTrue);
expect(localFile.readAsBytesSync(), [5, 6, 7, 8]);
});
test('returns 404 if dictionary api fails or has no audio', () async {
final mockClient = MockClient((request) async {
return http.Response('[]', 200);
});
final handler = spellingAudioHandler(httpClient: mockClient);
final request = Request('GET', Uri.parse('http://localhost/v1/spelling/audio/nonexistent.mp3'));
final response = await handler(request);
expect(response.statusCode, 404);
});
test('falls back to Merriam-Webster Collegiate API if dictionaryapi.dev returns empty/fails', () async {
final mockClient = MockClient((request) async {
if (request.url.path.contains('/entries/en/apple')) {
return http.Response('[]', 200); // dictionaryapi.dev returns no entries
}
if (request.url.path.contains('/references/collegiate/json/apple')) {
expect(request.url.queryParameters['key'], 'TEST_KEY');
const mwResponse = '''
[
{
"hwi": {
"prs": [
{
"sound": {
"audio": "apple"
}
}
]
}
}
]
''';
return http.Response(mwResponse, 200);
}
if (request.url.toString() == 'https://media.merriam-webster.com/audio/prons/en/us/mp3/a/apple.mp3') {
return http.Response.bytes([9, 10, 11, 12], 200);
}
return http.Response('Not found', 404);
});
final handler = spellingAudioHandler(merriamWebsterApiKey: 'TEST_KEY', httpClient: mockClient);
final request = Request('GET', Uri.parse('http://localhost/v1/spelling/audio/apple.mp3'));
final response = await handler(request);
expect(response.statusCode, 200);
expect(response.headers['Content-Type'], 'audio/mpeg');
expect(await response.read().first, [9, 10, 11, 12]);
final File localFile = File('${tempCacheDir.path}/apple.mp3');
expect(localFile.existsSync(), isTrue);
expect(localFile.readAsBytesSync(), [9, 10, 11, 12]);
});
test('logs failures to spelling_audio_errors.log when APIs return non-200 or empty data', () async {
final mockClient = MockClient((request) async {
if (request.url.path.contains('/entries/en/failedword')) {
return http.Response('Server Error', 500);
}
return http.Response('Not found', 404);
});
final handler = spellingAudioHandler(httpClient: mockClient);
final request = Request('GET', Uri.parse('http://localhost/v1/spelling/audio/failedword.mp3'));
final response = await handler(request);
expect(response.statusCode, 404);
expect(logFile.existsSync(), isTrue);
final String logContents = logFile.readAsStringSync();
expect(logContents, contains('=== Audio Fetch Failure ==='));
expect(logContents, contains('Word: failedword'));
expect(logContents, contains('API: dictionaryapi.dev'));
expect(logContents, contains('Status Code: 500'));
expect(logContents, contains('Response Body: Server Error'));
});
test('falls back to Google Cloud TTS if both dictionaries return empty/fails', () async {
final mockClient = MockClient((request) async {
if (request.url.path.contains('/entries/en/orange')) {
return http.Response('[]', 200);
}
if (request.url.path.contains('/references/collegiate/json/orange')) {
return http.Response('[]', 200);
}
if (request.url.path.contains('/text:synthesize')) {
expect(request.url.queryParameters['key'], 'GOOGLE_KEY');
expect(request.body, contains('"text":"orange"'));
const gcResponse = '''
{
"audioContent": "ZGVtb2J5dGVz"
}
''';
return http.Response(gcResponse, 200);
}
return http.Response('Not found', 404);
});
final handler = spellingAudioHandler(
merriamWebsterApiKey: 'TEST_KEY',
googleCloudApiKey: 'GOOGLE_KEY',
httpClient: mockClient,
);
final request = Request('GET', Uri.parse('http://localhost/v1/spelling/audio/orange.mp3'));
final response = await handler(request);
expect(response.statusCode, 200);
expect(response.headers['Content-Type'], 'audio/mpeg');
// base64Decode("ZGVtb2J5dGVz") -> "demobytes" -> [100, 101, 109, 111, 98, 121, 116, 101, 115]
expect(await response.read().first, [100, 101, 109, 111, 98, 121, 116, 101, 115]);
final File localFile = File('${tempCacheDir.path}/orange.mp3');
expect(localFile.existsSync(), isTrue);
expect(localFile.readAsBytesSync(), [100, 101, 109, 111, 98, 121, 116, 101, 115]);
});
});
}