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]); }); }); }