import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:chat_server/config/config.dart'; import 'package:chat_server/server.dart'; import 'package:chat_server/src/state/app_state.dart'; import 'package:test/test.dart'; void main() { final repoDir = Directory.current.path; final configPath = '$repoDir${Platform.pathSeparator}config.yaml'; final objectboxDir = '$repoDir${Platform.pathSeparator}objectbox'; final uploadsDir = '$repoDir${Platform.pathSeparator}uploads'; late String originalConfig; late Uri baseUri; final random = Random(); Future waitUntilServerReady(Uri uri) async { final client = HttpClient(); try { for (var i = 0; i < 120; i++) { try { final req = await client.getUrl(uri.resolve('/.well-known/server')); final res = await req.close(); if (res.statusCode == 200) { await res.drain(); return; } await res.drain(); } catch (_) {} await Future.delayed(const Duration(milliseconds: 250)); } fail('Server did not become ready in time.'); } finally { client.close(force: true); } } setUpAll(() async { originalConfig = File(configPath).existsSync() ? File(configPath).readAsStringSync() : ''; final bindPort = 12000 + random.nextInt(2000); final testConfig = ''' server: name: "Reliquary Test Server" bind_host: "127.0.0.1" bind_port: $bindPort public_base_url: "http://127.0.0.1:$bindPort" configured: true first_setup: true bootstrap_admin_limit: 2 dev_db_tools: true security: jwt_secret: "integration_test_secret_1234567890" '''; File(configPath).writeAsStringSync(testConfig); baseUri = Uri.parse('http://127.0.0.1:$bindPort'); final dataMdb = File('$objectboxDir${Platform.pathSeparator}data.mdb'); final lockMdb = File('$objectboxDir${Platform.pathSeparator}lock.mdb'); if (dataMdb.existsSync()) { dataMdb.deleteSync(); } if (lockMdb.existsSync()) { lockMdb.deleteSync(); } final uploads = Directory(uploadsDir); if (uploads.existsSync()) { for (final entity in uploads.listSync()) { if (entity is File) { entity.deleteSync(); } } } Config.initialize(); initializeStore(); unawaited(startServer()); await waitUntilServerReady(baseUri); }); tearDownAll(() async { await stopServer(); if (originalConfig.isEmpty) { File(configPath).deleteSync(); } else { File(configPath).writeAsStringSync(originalConfig); } }); test('auth + upload + component messages + gallery index', () async { final client = HttpClient(); try { Future> postJson( String path, Map body, { String? token, }) async { final req = await client.postUrl(baseUri.resolve(path)); req.headers.set(HttpHeaders.contentTypeHeader, 'application/json'); if (token != null) { req.headers.set(HttpHeaders.authorizationHeader, 'Bearer $token'); } req.write(jsonEncode(body)); final res = await req.close(); final text = await utf8.decodeStream(res); expect(res.statusCode, anyOf(200, 201), reason: '$path -> $text'); return jsonDecode(text) as Map; } Future> getJson( String path, { String? token, }) async { final req = await client.getUrl(baseUri.resolve(path)); if (token != null) { req.headers.set(HttpHeaders.authorizationHeader, 'Bearer $token'); } final res = await req.close(); final text = await utf8.decodeStream(res); expect(res.statusCode, 200, reason: '$path -> $text'); return jsonDecode(text) as Map; } Future> uploadFile( String filePath, { required String token, }) async { final file = File(filePath); expect(file.existsSync(), isTrue, reason: 'Missing test asset: $filePath'); final bytes = file.readAsBytesSync(); final rawFileName = file.uri.pathSegments.last; final safeFileName = rawFileName.replaceAll(RegExp(r'[^a-zA-Z0-9._-]'), '_'); final lower = rawFileName.toLowerCase(); final contentType = lower.endsWith('.mp4') ? 'video/mp4' : lower.endsWith('.png') ? 'image/png' : lower.endsWith('.jpg') || lower.endsWith('.jpeg') ? 'image/jpeg' : lower.endsWith('.gif') ? 'image/gif' : 'application/octet-stream'; final boundary = '----reliquary-test-boundary'; final req = await client.postUrl(baseUri.resolve('/api/uploads')); req.headers.set(HttpHeaders.authorizationHeader, 'Bearer $token'); req.headers.set( HttpHeaders.contentTypeHeader, 'multipart/form-data; boundary=$boundary', ); req.write('--$boundary\r\n'); req.write( 'Content-Disposition: form-data; name="file"; filename="$safeFileName"\r\n', ); req.write('Content-Type: $contentType\r\n\r\n'); req.add(bytes); req.write('\r\n--$boundary--\r\n'); final res = await req.close(); final text = await utf8.decodeStream(res); expect(res.statusCode, 200, reason: '/api/uploads -> $text'); return jsonDecode(text) as Map; } final register = await postJson('/auth/register', { 'username': 'alice', 'password': 'password123', }); final token = register['token'] as String; final channel = await postJson( '/api/channels', {'name': 'integration-channel', 'description': 'integration'}, token: token, ); final channelId = channel['id'] as int; final uploadedPng = await uploadFile( '$repoDir${Platform.pathSeparator}scripts${Platform.pathSeparator}media${Platform.pathSeparator}Untitled-1.png', token: token, ); final pngAttachmentId = uploadedPng['attachmentId'] as String; final uploadedPsd = await uploadFile( '$repoDir${Platform.pathSeparator}scripts${Platform.pathSeparator}media${Platform.pathSeparator}ftnr-symbol.psd', token: token, ); final psdAttachmentId = uploadedPsd['attachmentId'] as String; expect(psdAttachmentId, isNotEmpty); final uploadedMp4 = await uploadFile( '$repoDir${Platform.pathSeparator}scripts${Platform.pathSeparator}media${Platform.pathSeparator}Zelsus: Enemy Within [jUKjVrDFUug].mp4', token: token, ); final mp4AttachmentId = uploadedMp4['attachmentId'] as String; expect(mp4AttachmentId, isNotEmpty); final localMessage = await postJson( '/api/messages', { 'channelId': channelId, 'components': [ { 'id': 'cmp_local_gallery', 'type': 'gallery', 'typeVersion': 1, 'fallback': 'Local upload gallery', 'data': { 'items': [ { 'id': 'itm_local_1', 'source': 'local_upload', 'attachmentId': pngAttachmentId, } ] } } ] }, token: token, ); expect(localMessage['componentsJson'], contains('"source":"local_upload"')); expect(localMessage['componentsJson'], contains('"status":"ready"')); final fileAndVideoMessage = await postJson( '/api/messages', { 'channelId': channelId, 'components': [ { 'id': 'cmp_files', 'type': 'gallery', 'typeVersion': 1, 'fallback': 'Local file + local video + external file link', 'data': { 'items': [ { 'id': 'itm_psd', 'source': 'local_upload', 'attachmentId': psdAttachmentId, 'caption': 'PSD unsupported preview candidate' }, {'id': 'itm_mp4', 'source': 'local_upload', 'attachmentId': mp4AttachmentId, 'caption': 'MP4 playback candidate'}, { 'id': 'itm_ext_file', 'source': 'external_url', 'url': 'https://example.com/assets/archive.zip', 'mimeType': 'application/zip', 'originalFileName': 'archive.zip' } ] } } ] }, token: token, ); expect(fileAndVideoMessage['componentsJson'], contains('"itm_psd"')); expect(fileAndVideoMessage['componentsJson'], contains('"itm_mp4"')); expect(fileAndVideoMessage['componentsJson'], contains('"external_url"')); final externalMessage = await postJson( '/api/messages', { 'channelId': channelId, 'components': [ { 'id': 'cmp_external_gallery', 'type': 'gallery', 'typeVersion': 1, 'fallback': 'External gallery', 'data': { 'items': [ { 'id': 'itm_ext_1', 'source': 'external_url', 'url': 'https://pbs.twimg.com/media/HCI8WPKaYAAKhkj?format=jpg&name=medium', 'mimeType': 'image/jpeg', 'originalFileName': 'HCI8WPKaYAAKhkj.jpg', }, { 'id': 'itm_ext_2', 'source': 'external_url', 'url': 'https://pbs.twimg.com/media/HCKAMLxaUAAhYJS?format=jpg&name=medium', 'mimeType': 'image/jpeg', 'originalFileName': 'HCKAMLxaUAAhYJS.jpg', } ] } } ] }, token: token, ); expect(externalMessage['componentsJson'], contains('"source":"external_url"')); expect(externalMessage['componentsJson'], contains('"status":"ready"')); final messages = await getJson('/api/messages?channelId=$channelId&limit=10', token: token); final messageList = (messages['messages'] as List).cast>(); expect(messageList.length, greaterThanOrEqualTo(2)); final media = await getJson('/api/channels/$channelId/media?limit=10', token: token); final mediaItems = (media['media'] as List).cast>(); expect(mediaItems.length, greaterThanOrEqualTo(6)); final hasExternal = mediaItems.any((m) => (m['externalUrl'] as String?)?.contains('pbs.twimg.com/media/HCI8WPKaYAAKhkj') == true); final hasPngLocal = mediaItems.any((m) => (m['attachmentId'] as String?) == pngAttachmentId); final hasPsdLocal = mediaItems.any((m) => (m['attachmentId'] as String?) == psdAttachmentId); final hasMp4Local = mediaItems.any((m) => (m['attachmentId'] as String?) == mp4AttachmentId); final hasExternalFile = mediaItems.any((m) => (m['externalUrl'] as String?) == 'https://example.com/assets/archive.zip'); final hasVideoKind = mediaItems.any((m) => (m['attachmentId'] as String?) == mp4AttachmentId && m['kind'] == 'video'); expect(hasExternal, isTrue); expect(hasPngLocal, isTrue); expect(hasPsdLocal, isTrue); expect(hasMp4Local, isTrue); expect(hasExternalFile, isTrue); expect(hasVideoKind, isTrue); } finally { client.close(force: true); } }); }