327 lines
12 KiB
Dart
327 lines
12 KiB
Dart
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<void> 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<void>.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<Map<String, dynamic>> postJson(
|
||
String path,
|
||
Map<String, dynamic> 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<String, dynamic>;
|
||
}
|
||
|
||
Future<Map<String, dynamic>> 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<String, dynamic>;
|
||
}
|
||
|
||
Future<Map<String, dynamic>> 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<String, dynamic>;
|
||
}
|
||
|
||
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<Map<String, dynamic>>();
|
||
expect(messageList.length, greaterThanOrEqualTo(2));
|
||
|
||
final media = await getJson('/api/channels/$channelId/media?limit=10', token: token);
|
||
final mediaItems = (media['media'] as List).cast<Map<String, dynamic>>();
|
||
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);
|
||
}
|
||
});
|
||
}
|