210 lines
7.5 KiB
Dart
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]);
|
|
});
|
|
});
|
|
}
|