Files
chat-server/test/integration_api_test.dart

327 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
});
}