mirror of
https://github.com/trailbaseio/trailbase.git
synced 2025-12-21 09:29:44 -06:00
450 lines
12 KiB
Dart
450 lines
12 KiB
Dart
import 'dart:io';
|
|
import 'dart:convert';
|
|
|
|
import 'package:trailbase/trailbase.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:dio/dio.dart';
|
|
|
|
const port = 4006;
|
|
|
|
class SimpleStrict {
|
|
final String id;
|
|
|
|
final String? textNull;
|
|
final String textDefault;
|
|
final String textNotNull;
|
|
|
|
SimpleStrict({
|
|
required this.id,
|
|
this.textNull,
|
|
this.textDefault = '',
|
|
required this.textNotNull,
|
|
});
|
|
|
|
SimpleStrict.fromJson(Map<String, dynamic> json)
|
|
: id = json['id'],
|
|
textNull = json['text_null'],
|
|
textDefault = json['text_default'],
|
|
textNotNull = json['text_not_null'];
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
return other is SimpleStrict &&
|
|
id == other.id &&
|
|
textNull == other.textNull &&
|
|
textDefault == other.textDefault &&
|
|
textNotNull == other.textNotNull;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return Object.hash(id, textNull, textDefault, textNotNull);
|
|
}
|
|
|
|
@override
|
|
String toString() =>
|
|
'SimpleStrict(id: ${id}, textNull: ${textNull}, textDefault: ${textDefault}, textNotNull: ${textNotNull})';
|
|
}
|
|
|
|
class Author {
|
|
final String id;
|
|
final String user;
|
|
final String name;
|
|
|
|
Author.fromJson(Map<String, dynamic> json)
|
|
: id = json['id'],
|
|
user = json['user'],
|
|
name = json['name'];
|
|
|
|
@override
|
|
bool operator ==(Object rhs) {
|
|
return rhs is Author &&
|
|
rhs.id == id &&
|
|
rhs.user == user &&
|
|
rhs.name == name;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return Object.hash(id, user, name);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'Author(${id}, ${user}, ${name})';
|
|
}
|
|
}
|
|
|
|
class Post {
|
|
final String id;
|
|
final String author;
|
|
final String title;
|
|
final String body;
|
|
|
|
Post.fromJson(Map<String, dynamic> json)
|
|
: id = json['id'],
|
|
author = json['author'],
|
|
title = json['title'],
|
|
body = json['body'];
|
|
|
|
@override
|
|
bool operator ==(Object rhs) {
|
|
return rhs is Post &&
|
|
rhs.id == id &&
|
|
rhs.author == author &&
|
|
rhs.title == title &&
|
|
rhs.body == body;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return Object.hash(id, author, title, body);
|
|
}
|
|
}
|
|
|
|
class Comment {
|
|
final int id;
|
|
final String body;
|
|
final (String, Post?) post;
|
|
final (String, Author?) author;
|
|
|
|
Comment({
|
|
required this.id,
|
|
required this.body,
|
|
required String postId,
|
|
Map<String, dynamic>? post,
|
|
required String authorId,
|
|
Map<String, dynamic>? authorProfile,
|
|
}) : post = (postId, post != null ? Post.fromJson(post) : null),
|
|
author = (
|
|
authorId,
|
|
authorProfile != null ? Author.fromJson(authorProfile) : null
|
|
);
|
|
|
|
Comment.fromJson(Map<String, dynamic> json)
|
|
: this(
|
|
id: json['id'],
|
|
body: json['body'],
|
|
postId: json['post']['id'],
|
|
post: json['post']['data'],
|
|
authorId: json['author']['id'],
|
|
authorProfile: json['author']['data'],
|
|
);
|
|
|
|
@override
|
|
bool operator ==(Object rhs) {
|
|
return rhs is Comment &&
|
|
rhs.id == id &&
|
|
rhs.body == body &&
|
|
rhs.post == post &&
|
|
rhs.author == author;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return Object.hash(id, body, post, author);
|
|
}
|
|
|
|
@override
|
|
String toString() => 'Comment(${id}, ${body}, ${post}, ${author})';
|
|
}
|
|
|
|
Future<Client> connect() async {
|
|
final client = Client('http://127.0.0.1:${port}');
|
|
await client.login('admin@localhost', 'secret');
|
|
return client;
|
|
}
|
|
|
|
Future<Process> initTrailBase() async {
|
|
final result = await Process.run('cargo', ['build'],
|
|
stdoutEncoding: utf8, stderrEncoding: utf8);
|
|
if (result.exitCode > 0) {
|
|
throw Exception(
|
|
'Cargo build failed.\n\nstdout: ${result.stdout}\n\nstderr: ${result.stderr}\n');
|
|
}
|
|
|
|
// Relative to CWD.
|
|
const depotPath = '../testfixture';
|
|
|
|
final process = await Process.start('cargo', [
|
|
'run',
|
|
'--',
|
|
'--data-dir=${depotPath}',
|
|
'run',
|
|
'--address=127.0.0.1:${port}',
|
|
// We want at least some parallelism to experience isolate-local state.
|
|
'--js-runtime-threads=2',
|
|
]);
|
|
|
|
final dio = Dio();
|
|
for (int i = 0; i < 100; ++i) {
|
|
try {
|
|
final response = await dio.fetch(
|
|
RequestOptions(path: 'http://127.0.0.1:${port}/api/healthcheck'));
|
|
if (response.statusCode == 200) {
|
|
return process;
|
|
}
|
|
} catch (err) {
|
|
print('Trying to connect to TrailBase');
|
|
}
|
|
|
|
if (await process.exitCode
|
|
.timeout(Duration(milliseconds: 500), onTimeout: () => -1) >=
|
|
0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
process.kill(ProcessSignal.sigkill);
|
|
final exitCode = await process.exitCode;
|
|
|
|
await process.stderr.forEach(stdout.add);
|
|
await process.stdout.forEach(stdout.add);
|
|
|
|
throw Exception('Cargo run failed: ${exitCode}.');
|
|
}
|
|
|
|
Future<void> main() async {
|
|
if (!Directory.current.path.endsWith('dart')) {
|
|
throw Exception('Unexpected working directory');
|
|
}
|
|
|
|
final process = await initTrailBase();
|
|
|
|
tearDownAll(() async {
|
|
process.kill(ProcessSignal.sigkill);
|
|
final _ = await process.exitCode;
|
|
|
|
// await process.stderr.forEach(stdout.add);
|
|
// await process.stdout.forEach(stdout.add);
|
|
});
|
|
|
|
group('client tests', () {
|
|
test('auth', () async {
|
|
final client = await connect();
|
|
|
|
final oldTokens = client.tokens();
|
|
expect(oldTokens, isNotNull);
|
|
expect(oldTokens!.valid, isTrue);
|
|
|
|
final user = client.user()!;
|
|
expect(user.id, isNot(equals('')));
|
|
expect(user.email, equals('admin@localhost'));
|
|
|
|
await client.logout();
|
|
expect(client.tokens(), isNull);
|
|
|
|
// We need to wait a little to push the expiry time in seconds to avoid just getting the same token minted again.
|
|
await Future.delayed(Duration(milliseconds: 1500));
|
|
|
|
final newTokens = await client.login('admin@localhost', 'secret');
|
|
expect(newTokens, isNotNull);
|
|
expect(newTokens.valid, isTrue);
|
|
|
|
expect(newTokens, isNot(equals(oldTokens)));
|
|
|
|
await client.refreshAuthToken();
|
|
expect(newTokens, equals(client.tokens()));
|
|
});
|
|
|
|
test('records', () async {
|
|
final client = await connect();
|
|
final api = client.records('simple_strict_table');
|
|
|
|
final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
final messages = [
|
|
'dart client test 0: =?&${now}',
|
|
'dart client test 1: =?&${now}',
|
|
];
|
|
final ids = [];
|
|
for (final msg in messages) {
|
|
ids.add(await api.create({'text_not_null': msg}));
|
|
}
|
|
|
|
{
|
|
// Bulk
|
|
final ids = await api.createBulk([
|
|
{'text_not_null': 'dart 1st bulk'},
|
|
{'text_not_null': 'dart 2nd bulk'},
|
|
]);
|
|
expect(ids.length, 2);
|
|
}
|
|
|
|
{
|
|
final response = await api.list(
|
|
filters: [Filter(column: 'text_not_null', value: messages[0])],
|
|
);
|
|
expect(response.records.length, 1);
|
|
expect(response.records[0]['text_not_null'], messages[0]);
|
|
}
|
|
|
|
{
|
|
final recordsAsc = (await api.list(
|
|
order: ['+text_not_null'],
|
|
filters: [
|
|
Filter(
|
|
column: 'text_not_null',
|
|
op: CompareOp.like,
|
|
value: '% =?&${now}')
|
|
],
|
|
))
|
|
.records;
|
|
expect(recordsAsc.map((el) => el['text_not_null']),
|
|
orderedEquals(messages));
|
|
|
|
final recordsDesc = (await api.list(
|
|
order: ['-text_not_null'],
|
|
filters: [
|
|
Filter(
|
|
column: 'text_not_null', op: CompareOp.like, value: '%${now}')
|
|
],
|
|
))
|
|
.records;
|
|
expect(recordsDesc.map((el) => el['text_not_null']).toList().reversed,
|
|
orderedEquals(messages));
|
|
}
|
|
|
|
{
|
|
final response = (await api.list(
|
|
pagination: Pagination(limit: 1),
|
|
order: ['-text_not_null'],
|
|
filters: [
|
|
Filter(
|
|
column: 'text_not_null', op: CompareOp.like, value: '%${now}')
|
|
],
|
|
count: true,
|
|
));
|
|
|
|
expect(response.totalCount ?? -1, 2);
|
|
// Ensure there's no extra field, i.e the count doesn't get serialized.
|
|
expect(response.records[0].keys.length, 13);
|
|
}
|
|
|
|
final record = SimpleStrict.fromJson(await api.read(ids[0]));
|
|
|
|
expect(ids[0] == record.id, isTrue);
|
|
// Note: the .id() is needed otherwise we call String's operator==. It's not ideal
|
|
// but we didn't come up with a better option.
|
|
expect(record.id.id() == ids[0], isTrue);
|
|
expect(RecordId.uuid(record.id) == ids[0], isTrue);
|
|
|
|
expect(record.textNotNull, messages[0]);
|
|
|
|
final updatedMessage = 'dart client updated test 0: ${now}';
|
|
await api.update(ids[0], {'text_not_null': updatedMessage});
|
|
final updatedRecord = SimpleStrict.fromJson(await api.read(ids[0]));
|
|
expect(updatedRecord.textNotNull, updatedMessage);
|
|
|
|
await api.delete(ids[0]);
|
|
expect(() async => await api.read(ids[0]), throwsException);
|
|
});
|
|
|
|
test('expand foreign records', () async {
|
|
final client = await connect();
|
|
final api = client.records('comment');
|
|
|
|
{
|
|
final comment = Comment.fromJson(await api.read(RecordId.integer(1)));
|
|
|
|
expect(comment.id, equals(1));
|
|
expect(comment.body, equals('first comment'));
|
|
expect(comment.author.$2, isNull);
|
|
expect(comment.post.$2?.title, isNull);
|
|
}
|
|
|
|
{
|
|
final comment = Comment.fromJson(await api.read(
|
|
RecordId.integer(1),
|
|
expand: ['post'],
|
|
));
|
|
|
|
expect(comment.id, equals(1));
|
|
expect(comment.body, equals('first comment'));
|
|
expect(comment.author.$2, isNull);
|
|
expect(comment.post.$2?.title, equals('first post'));
|
|
}
|
|
|
|
{
|
|
final response = await api.list(
|
|
expand: ['author', 'post'],
|
|
order: ['-id'],
|
|
pagination: Pagination(limit: 2),
|
|
);
|
|
|
|
expect(response.records.length, equals(2));
|
|
final first = Comment.fromJson(response.records[0]);
|
|
expect(first.id, equals(2));
|
|
expect(first.body, equals('second comment'));
|
|
expect(first.author.$2?.name, equals('SecondUser'));
|
|
expect(first.post.$2?.title, equals('first post'));
|
|
|
|
final second = Comment.fromJson(response.records[1]);
|
|
|
|
final offsetResponse = await api.list(
|
|
expand: ['author', 'post'],
|
|
order: ['-id'],
|
|
pagination: Pagination(limit: 1, offset: 1),
|
|
);
|
|
|
|
expect(offsetResponse.records.length, equals(1));
|
|
expect(Comment.fromJson(offsetResponse.records[0]), equals(second));
|
|
}
|
|
});
|
|
|
|
test('realtime', () async {
|
|
final client = await connect();
|
|
final api = client.records('simple_strict_table');
|
|
|
|
final tableEvents = await api.subscribeAll();
|
|
|
|
final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
final createMessage = 'dart client realtime test 0: =?&${now}';
|
|
final id = await api.create({'text_not_null': createMessage});
|
|
|
|
final events = await api.subscribe(id);
|
|
|
|
final updatedMessage = 'dart client updated realtime test 0: ${now}';
|
|
await api.update(id, {'text_not_null': updatedMessage});
|
|
await api.delete(id);
|
|
|
|
final eventList =
|
|
await events.timeout(Duration(seconds: 10), onTimeout: (sink) {
|
|
print('Stream timeout');
|
|
sink.close();
|
|
}).toList();
|
|
|
|
expect(eventList.length, equals(2));
|
|
expect(eventList[0].runtimeType, equals(UpdateEvent));
|
|
expect(
|
|
SimpleStrict.fromJson(eventList[0].value()!),
|
|
SimpleStrict(
|
|
id: id.toString(),
|
|
textNotNull: updatedMessage,
|
|
));
|
|
|
|
expect(eventList[1].runtimeType, equals(DeleteEvent));
|
|
expect(
|
|
SimpleStrict.fromJson(eventList[1].value()!),
|
|
SimpleStrict(
|
|
id: id.toString(),
|
|
textNotNull: updatedMessage,
|
|
));
|
|
|
|
final tableEventList =
|
|
await tableEvents.timeout(Duration(seconds: 10), onTimeout: (sink) {
|
|
print('Stream timeout');
|
|
sink.close();
|
|
}).toList();
|
|
expect(tableEventList.length, equals(3));
|
|
|
|
expect(tableEventList[0].runtimeType, equals(InsertEvent));
|
|
expect(
|
|
SimpleStrict.fromJson(tableEventList[0].value()!),
|
|
SimpleStrict(
|
|
id: id.toString(),
|
|
textNotNull: createMessage,
|
|
));
|
|
});
|
|
});
|
|
}
|