Testing is complete!! And a nice build runner script to boot

This commit is contained in:
Nathan Anderson 2025-02-05 13:16:12 -07:00
parent 4864de624c
commit 76ea825509
25 changed files with 1453 additions and 103 deletions

3
.gitignore vendored
View File

@ -68,6 +68,9 @@ app.*.map.json
# Misc # Misc
### ###
# Dartfrog
**/.dart_frog/**
# Direnv # Direnv
**/.direnv/** **/.direnv/**

View File

@ -2,3 +2,7 @@
LICENSE LICENSE
**/.ignore **/.ignore
**/.gitignore **/.gitignore
# Dont edit these often, so its just noise in the file picker
backend/test_e2e/pubspec.yaml
backend/scripts/pubspec.yaml

View File

@ -8,7 +8,7 @@ Dev is done all in dart, with the flutter framework as a frontend. The
backend uses [dart_frog](), a dart backend framework with a focus on developer backend uses [dart_frog](), a dart backend framework with a focus on developer
experience. This whole stack revolves around my Dart expertise, so its not about experience. This whole stack revolves around my Dart expertise, so its not about
being the fastest stack, but the quickest stack I can build in. Because building being the fastest stack, but the quickest stack I can build in. Because building
a product is like a fart: its all about follow-through. a product is like a fart: You can't be sure its not crap until the follow-through.
### Installing ### Installing
@ -16,6 +16,38 @@ If you have the nix package manager and direnv setup, its as easy as running `di
If not, well, you need to setup `flutter`, `dart` (should get installed with flutter), and `dart_frog` If not, well, you need to setup `flutter`, `dart` (should get installed with flutter), and `dart_frog`
### Running
This codebase makes use of [RPS](https://pub.dev/packages/rps/install), just to standardize calling
various tools, like Make, but for dart.
- Frontend is started with `cd frontend` and `flutter run` (not included in rps because there are lots of cli flags I change often with flutter run)
- Backend is started with `cd backend` and `rps dev` or `dart_frog dev`
- The `build_runners` (codegen) for each subproject can all be run `cd backend` and `rps watch` for development or `rps build` for a one off codegen
- If you dont want to bother installing rps, you can run the script directly:
```sh
dart backend/scripts/run_build_runner.dart
```
And if you dont want to run all three watchers, you can just run an individual one with
```sh
dart run build_runner watch # or other args
```
### Testing
#### Backend
End to end tests can be run with `cd backend` and `rps e2e`.
All end to end tests are located in the `backend/test_e2e/tests` folder.
There is a helper script that compiles and launches the backend and runs some end to end tests through.
#### Frontend
:|
## Frontend `./frontend/` ## Frontend `./frontend/`
WIP WIP

View File

@ -1,57 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, implicit_dynamic_list_literal
import 'dart:io';
import 'package:dart_frog/dart_frog.dart';
import '../main.dart' as entrypoint;
import '../routes/index.dart' as index;
import '../routes/create_room.dart' as create_room;
import '../routes/room/[roomCode]/join.dart' as room_$room_code_join;
import '../routes/auth/index.dart' as auth_index;
import '../routes/_middleware.dart' as middleware;
import '../routes/room/[roomCode]/_middleware.dart' as room_$room_code_middleware;
void main() async {
final address = InternetAddress.tryParse('') ?? InternetAddress.anyIPv6;
final port = int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080;
hotReload(() => createServer(address, port));
}
Future<HttpServer> createServer(InternetAddress address, int port) {
final handler = Cascade().add(buildRootHandler()).handler;
return entrypoint.run(handler, address, port);
}
Handler buildRootHandler() {
final pipeline = const Pipeline().addMiddleware(middleware.middleware);
final router = Router()
..mount('/auth', (context) => buildAuthHandler()(context))
..mount('/room/<roomCode>', (context,roomCode,) => buildRoom$roomCodeHandler(roomCode,)(context))
..mount('/', (context) => buildHandler()(context));
return pipeline.addHandler(router);
}
Handler buildAuthHandler() {
final pipeline = const Pipeline();
final router = Router()
..all('/', (context) => auth_index.onRequest(context,));
return pipeline.addHandler(router);
}
Handler buildRoom$roomCodeHandler(String roomCode,) {
final pipeline = const Pipeline().addMiddleware(room_$room_code_middleware.middleware);
final router = Router()
..all('/join', (context) => room_$room_code_join.onRequest(context,roomCode,));
return pipeline.addHandler(router);
}
Handler buildHandler() {
final pipeline = const Pipeline();
final router = Router()
..all('/', (context) => index.onRequest(context,))..all('/create_room', (context) => create_room.onRequest(context,));
return pipeline.addHandler(router);
}

View File

@ -6,6 +6,7 @@ linter:
prefer_single_quotes: true prefer_single_quotes: true
public_member_api_docs: false public_member_api_docs: false
lines_longer_than_80_chars: false lines_longer_than_80_chars: false
avoid_redundant_argument_values: false
analyzer: analyzer:
exclude: exclude:

View File

@ -4,6 +4,7 @@ import 'package:backend/database.dart';
import 'package:backend/service/db_access.dart'; import 'package:backend/service/db_access.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:shared_models/jwt.dart';
import 'package:shared_models/user.dart'; import 'package:shared_models/user.dart';
final jwtSecret = _getSecret(); final jwtSecret = _getSecret();
@ -18,21 +19,16 @@ enum JWTTokenStatus {
} }
class Authenticator { class Authenticator {
Future<String?> generateToken(CreateUserRequest req) async { Future<(String?, User?)> generateToken(CreateUserRequest req) async {
final newUser = await Db.createUser(username: req.username, roomCode: req.roomCode); final newUser = await Db.createUser(username: req.username, roomCode: req.roomCode);
if (newUser == null) return null; if (newUser == null) return (null, null);
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000; final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final jwt = JWT( final jwt = JWT(
{ JWTBody(uuid: newUser.uuid, roomUuid: newUser.gameRoomUuid, iat: iat, exp: iat + expTimeSecs).toJson(),
'uid': newUser.uuid,
'roomUuid': newUser.gameRoomUuid,
'iat': iat,
'exp': iat + expTimeSecs,
},
); );
return jwt.sign(SecretKey(jwtSecret)); return (jwt.sign(SecretKey(jwtSecret)), newUser);
} }
Future<(User?, JWTTokenStatus)> verifyToken( Future<(User?, JWTTokenStatus)> verifyToken(
@ -45,18 +41,15 @@ class Authenticator {
SecretKey(jwtSecret), SecretKey(jwtSecret),
); );
final payloadData = payload.payload as Map<String, dynamic>; final jwt = JWTBody.fromJson(payload.payload as Map<String, dynamic>);
final iat = payloadData['iat'] as int; if (jwt.iat + expTimeSecs != jwt.exp || DateTime.now().millisecondsSinceEpoch ~/ 1000 > jwt.exp) {
final exp = payloadData['exp'] as int;
if (iat + expTimeSecs != exp || DateTime.now().millisecondsSinceEpoch ~/ 1000 > exp) {
return (null, JWTTokenStatus.expired); return (null, JWTTokenStatus.expired);
} }
final uuid = payloadData['uuid'] as String; return (await Db.getUserById(jwt.uuid), JWTTokenStatus.valid);
return (await Db.getUser(uuid), JWTTokenStatus.valid);
} catch (e) { } catch (e) {
log.fine('Error verifying token', e);
return (null, JWTTokenStatus.invalid); return (null, JWTTokenStatus.invalid);
} }
} }

View File

@ -22,7 +22,7 @@ Middleware tokenAuthMiddleware({
} }
final auth = context.read<Authenticator>(); final auth = context.read<Authenticator>();
// use `auth.verifyToken(token)` to check the jwt that came in the request header bearer // use `auth.verifyToken(token)` to check the jwt that came in the request header bearer
final authHeader = context.request.headers['authorization']; final authHeader = context.request.headers['authorization'] ?? context.request.headers['Authorization'];
final auths = authHeader?.split(' '); final auths = authHeader?.split(' ');
if (authHeader == null || !authHeader.startsWith('Bearer ') || auths == null || auths.length != 2) { if (authHeader == null || !authHeader.startsWith('Bearer ') || auths == null || auths.length != 2) {
log.fine('Denied request - No Auth - ${context.request.method.value} ${context.request.uri.path}'); log.fine('Denied request - No Auth - ${context.request.method.value} ${context.request.uri.path}');
@ -34,7 +34,8 @@ Middleware tokenAuthMiddleware({
if (user == null) { if (user == null) {
log.fine( log.fine(
'Denied request - Bad Auth:$tokStatus - ${context.request.method.value} ${context.request.uri.path}, no auth'); 'Denied request - Bad Auth:$tokStatus - ${context.request.method.value} ${context.request.uri.path}',
);
return Response(statusCode: HttpStatus.unauthorized); return Response(statusCode: HttpStatus.unauthorized);
} }

View File

@ -8,9 +8,9 @@ final log = Logger('Db');
class Db { class Db {
static final _db = AppDatabase(); static final _db = AppDatabase();
static Future<User> getUser(String uuid) { static Future<User?> getUserById(String uuid) {
log.finer('Getting user $uuid'); log.finer('Getting user $uuid');
return _db.managers.users.filter((f) => f.uuid.equals(uuid)).getSingle(); return _db.managers.users.filter((f) => f.uuid.equals(uuid)).getSingleOrNull();
} }
static Future<User?> createUser({required String username, required String roomCode}) async { static Future<User?> createUser({required String username, required String roomCode}) async {
@ -32,14 +32,21 @@ class Db {
}); });
} }
static Future<GameRoom> createRoom({required String roomCode}) => _db.managers.gameRooms static Future<GameRoom?> createRoom({required String roomCode}) => _db.managers.gameRooms
.createReturning( .createReturningOrNull(
(o) => o(createdAt: Value(DateTime.now()), status: GameStatus.open, uuid: const Uuid().v4(), code: roomCode), (o) => o(createdAt: Value(DateTime.now()), status: GameStatus.open, uuid: const Uuid().v4(), code: roomCode),
) )
.catchError( .catchError(
(Object err) { (Object err) {
log.severe('Failed to create room', err, StackTrace.current); log.severe('Failed to create room', err, StackTrace.current);
throw Exception(err.toString()); return null;
}, },
); );
static Future<GameRoom?> getRoomByCode(String? roomCode) async {
final room = await _db.managers.gameRooms
.filter((f) => f.code.equals(roomCode) & f.status.isIn([GameStatus.open, GameStatus.running]))
.getSingleOrNull();
return room;
}
} }

View File

@ -302,6 +302,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.0" version: "4.3.0"
http:
dependency: "direct dev"
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_methods: http_methods:
dependency: transitive dependency: transitive
description: description:

View File

@ -20,6 +20,16 @@ dependencies:
dev_dependencies: dev_dependencies:
build_runner: ^2.4.14 build_runner: ^2.4.14
drift_dev: ^2.24.0 drift_dev: ^2.24.0
# For e2e testing
http: ^1.3.0
mocktail: ^1.0.3 mocktail: ^1.0.3
test: ^1.25.5 test: ^1.25.5
very_good_analysis: ^5.1.0 very_good_analysis: ^5.1.0
scripts:
run_build: dart scripts/run_build_runner.dart
run_watch: dart scripts/run_build_runner.dart --watch
build: dart_frog build
dev: dart_frog dev
e2e: dart scripts/test_runner.dart
test: dart test

View File

@ -6,7 +6,7 @@ import 'package:dart_frog/dart_frog.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:shared_models/room.dart'; import 'package:shared_models/room.dart';
final log = Logger('create_room'); final log = Logger('create_room/');
Future<Response> onRequest(RequestContext context) async { Future<Response> onRequest(RequestContext context) async {
// Only allow POST requests // Only allow POST requests
@ -28,6 +28,17 @@ Future<Response> onRequest(RequestContext context) async {
// Create the room // Create the room
final room = await Db.createRoom(roomCode: roomCode); final room = await Db.createRoom(roomCode: roomCode);
if (room == null) {
return Response.json(
statusCode: HttpStatus.internalServerError,
body: CreateRoomResponse(
error: 'Unexpected error: unable to create room',
success: false,
roomCode: null,
).toJson(),
);
}
// Return the room code // Return the room code
return Response.json( return Response.json(
body: CreateRoomResponse(success: true, roomCode: room.code).toJson(), body: CreateRoomResponse(success: true, roomCode: room.code).toJson(),

View File

@ -1,5 +1,5 @@
import 'package:dart_frog/dart_frog.dart'; import 'package:dart_frog/dart_frog.dart';
Response onRequest(RequestContext context) { Response onRequest(RequestContext context) {
return Response(body: 'Welcome to Dart Frog!'); return Response(statusCode: 200);
} }

View File

@ -5,7 +5,7 @@ import 'package:dart_frog/dart_frog.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:shared_models/user.dart'; import 'package:shared_models/user.dart';
final log = Logger('auth/'); final log = Logger('join_room/');
Future<Response> onRequest(RequestContext context) async { Future<Response> onRequest(RequestContext context) async {
// Only allow POST requests // Only allow POST requests
@ -20,33 +20,28 @@ Future<Response> onRequest(RequestContext context) async {
// Generate token // Generate token
final authenticator = context.read<Authenticator>(); final authenticator = context.read<Authenticator>();
final token = await authenticator.generateToken(createUserReq); final (token, user) = await authenticator.generateToken(createUserReq);
if (token == null) { if (token == null || user == null) {
final body = CreateUserResponse( final body = CreateUserResponse(
success: false, success: false,
token: null, token: null,
error: 'Room ${createUserReq.roomCode} requested is not available', error: user == null ? 'Room ${createUserReq.roomCode} requested is not available' : 'Unexpected error occurred',
uuid: null,
).toJson(); ).toJson();
return Response.json( return Response.json(
statusCode: HttpStatus.badRequest, statusCode: user == null ? HttpStatus.badRequest : HttpStatus.internalServerError,
body: body, body: body,
); );
} }
// Return the token // Return the token
return Response.json( return Response.json(
body: CreateUserResponse(token: token, success: true).toJson(), body: CreateUserResponse(token: token, success: true, uuid: user.uuid).toJson(),
); );
// }
// on JWTParseException {
// return Response.json(
// statusCode: HttpStatus.badRequest,
// body: {'error': 'Username is required'},
// );
} catch (e) { } catch (e) {
log.severe('Error:', e); log.severe('Error:', e, StackTrace.current);
final body = CreateUserResponse(success: false, token: null, error: 'Internal server error').toJson(); final body = CreateUserResponse(success: false, token: null, error: 'Internal server error', uuid: null).toJson();
return Response.json( return Response.json(
statusCode: HttpStatus.internalServerError, statusCode: HttpStatus.internalServerError,
body: body, body: body,

View File

@ -0,0 +1,10 @@
import 'package:backend/service/db_access.dart';
import 'package:dart_frog/dart_frog.dart';
Future<Response> onRequest(RequestContext context, String roomCode) async {
final gameRoom = await Db.getRoomByCode(roomCode);
if (gameRoom == null) {
return Response(statusCode: 404);
}
return Response(statusCode: 200);
}

View File

@ -0,0 +1,402 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c"
url: "https://pub.dev"
source: hosted
version: "79.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643
url: "https://pub.dev"
source: hosted
version: "7.2.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.11.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
http:
dependency: "direct dev"
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "8391fbe68d520daf2314121764d38e37f934c02fd7301ad18307bd93bd6b725d"
url: "https://pub.dev"
source: hosted
version: "1.25.14"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"

View File

@ -0,0 +1,8 @@
name: "e2e_test_dart_runner"
environment:
sdk: ">=3.0.0 <4.0.0"
dev_dependencies:
test: ^1.24.0
http: ^1.1.0

View File

@ -0,0 +1,178 @@
// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:io';
const packages = ['backend', 'frontend', 'shared_models'];
Map<int, Process> runnerWatchProcesses = {};
void main(List<String> args) async {
// Get args and create bool for if `--watch` passed in
final watch = args.contains('--watch');
if (watch) {
print('👀 Running build_runner in watch mode for all packages...\n');
} else {
print('🏃 Running build_runner in build mode for all packages...\n');
}
final projectRootPath = await Process.run(
'git',
['rev-parse', '--show-toplevel'],
).then((result) => result.stdout.toString().trim());
for (final package in packages) {
unawaited(runBuildRunner('$projectRootPath/$package', watch: watch));
}
await Future<void>.delayed(const Duration(seconds: 1));
var i = 0;
var j = 0;
while (runnerWatchProcesses.isNotEmpty) {
await Future<void>.delayed(const Duration(seconds: 1));
i++;
if (i == 10) {
if (!watch) print(' Waiting for build to finish... ${runnerWatchProcesses.keys}');
if (packages.length != runnerWatchProcesses.entries.length) {
j++;
} else {
j = 0;
}
if (j == 2) {
print(
'Missing build runners for too long in tracked processes. Something probably went wrong. Check logs and fix the script.',
);
exitRunners();
}
}
}
print('\n✅ All build_runner tasks completed successfully!');
}
Future<void> runBuildRunner(String packagePath, {required bool watch}) async {
final package = packagePath.split('/').last;
print('📦 Starting build_runner for $package...');
Future<(Process, bool)> buildRunnerFunc() => watch ? _runBuildRunnerWatch(packagePath) : _runBuildRunner(packagePath);
const maxAttempts = 5;
Future<void> attemptRecover(Object? e, int i) async {
try {
print('\n⚠️ Error in $package:\n$e\nAttempting recovery (${i + 1} / $maxAttempts)...\n');
print(' Waiting 10 seconds before retry...');
await Future<void>.delayed(const Duration(seconds: 1));
// Recovery attempt
print(' Running pub get...');
await Process.run(
package == 'frontend' ? 'flutter' : 'dart',
['pub', 'get'],
workingDirectory: packagePath,
);
print(' Retrying build_runner...');
return;
} catch (e) {
print('Error while trying to recover $package: $e');
}
}
Object? err;
for (var i = 0; i < maxAttempts; i++) {
try {
final (process, needsRestart) = await buildRunnerFunc();
final removed = runnerWatchProcesses.remove(process.pid);
assert(removed != null, true);
if (needsRestart) {
await attemptRecover(process.stderr, i);
} else {
return;
}
} catch (e) {
await attemptRecover(e, i);
}
}
print('\n❌ Failed to recover $packagePath: $err');
exitRunners();
}
Future<(Process, bool)> _runBuildRunner(String package) async {
final process = await Process.start(
'dart',
['run', 'build_runner', 'build', '--delete-conflicting-outputs'],
workingDirectory: package,
);
runnerWatchProcesses.addAll({process.pid: process});
final outputLines = <String>[];
final errorLines = <String>[];
process.stdout.transform(const SystemEncoding().decoder).listen(outputLines.add);
process.stderr.transform(const SystemEncoding().decoder).listen(errorLines.add);
final exitCode = await process.exitCode;
if (exitCode != 0) {
// Print filtered error output
final relevantErrors = errorLines
.where((line) => line.contains('ERROR') || line.contains('Exception') || line.contains('Failed'))
.join('\n');
print('Build runner failed for $package:\n$relevantErrors');
return (process, true);
}
// Print success with minimal output
final successMessage =
outputLines.where((line) => line.contains('Succeeded') || line.contains('Generated')).join('\n').trim();
print(' ${successMessage.isEmpty ? "Completed successfully" : successMessage}');
return (process, false);
}
Future<(Process, bool)> _runBuildRunnerWatch(String packagePath) async {
final package = packagePath.split('/').last;
final process = await Process.start(
'dart',
['run', 'build_runner', 'watch', '--delete-conflicting-outputs'],
workingDirectory: packagePath,
);
runnerWatchProcesses.addAll({process.pid: process});
var restart = false;
process.stdout.transform(const SystemEncoding().decoder).listen((line) {
if (line.contains('Succeeded') || line.contains('Generated')) {
print('🎯 [${package.toUpperCase()}] - ${line.trim()}');
}
if (line.contains('SEVERE')) {
print('🚩 [${package.toUpperCase()}] - ${line.trim()}');
restart = true;
}
});
process.stderr.transform(const SystemEncoding().decoder).listen((line) {
if (line.contains('ERROR') || line.contains('Exception') || line.contains('Failed')) {
print('🎯 [${package.toUpperCase()}] - ${line.trim()}');
restart = true;
}
});
final code = await process.exitCode;
if (!restart) {
restart = code != 0;
}
return (process, restart);
}
void exitRunners() {
print('❌ Killing processes');
for (final pEntry in runnerWatchProcesses.entries) {
pEntry.value.kill();
}
exit(1);
}

View File

@ -0,0 +1,76 @@
import 'dart:async';
import 'dart:io';
void main() async {
final projectRootPath = await Process.run(
'git',
['rev-parse', '--show-toplevel'],
).then((result) => result.stdout.toString().trim());
final backendPath = '$projectRootPath/backend';
// Start the server
print('Starting Dart Frog server...');
final build = await Process.start(
'dart_frog',
['build'],
workingDirectory: backendPath,
mode: ProcessStartMode.inheritStdio,
);
List<String> serverLogs = [];
final server = await Process.start('dart', ['build/bin/server.dart'], workingDirectory: backendPath);
server.stdout.transform(const SystemEncoding().decoder).listen((line) => serverLogs.add(line));
server.stderr.transform(const SystemEncoding().decoder).listen((line) => serverLogs.add(line));
// Wait for server to be ready
print('Waiting for server to be ready...');
await _waitForServer();
print('Running tests...');
try {
// Run the tests
final testResult = await Process.run('dart', ['test', '$backendPath/test_e2e/tests/']);
stdout.write(testResult.stdout);
stderr.write(testResult.stderr);
// Kill the server regardless of test result
server.kill();
if (testResult.exitCode != 0) {
final sub = serverLogs.length > 10 ? serverLogs.length - 10 : 0;
stdout.write("Server logs:\n${serverLogs.sublist(sub).join('\n')}");
}
// Exit with the same code as the test process
exit(testResult.exitCode);
} catch (e) {
print('Error running tests: $e');
build.kill();
server.kill();
exit(1);
}
}
Future<void> _waitForServer() async {
final client = HttpClient();
const maxAttempts = 30; // 30 seconds timeout
var attempts = 0;
while (attempts < maxAttempts) {
try {
final request =
await client.getUrl(Uri.parse('http://localhost:8080/health')).timeout(const Duration(seconds: 1));
final response = await request.close();
await response.drain<void>();
if (response.statusCode == 200) {
print('Server is ready!');
return;
}
} catch (e) {
attempts++;
await Future.delayed(const Duration(seconds: 1));
}
}
throw Exception('Server failed to start after $maxAttempts seconds');
}

View File

@ -4,19 +4,20 @@ import 'package:dart_frog/dart_frog.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../../routes/index.dart' as route; import '../../routes/health.dart' as route;
class _MockRequestContext extends Mock implements RequestContext {} class _MockRequestContext extends Mock implements RequestContext {}
void main() { void main() {
group('GET /', () { group('GET /health', () {
test('responds with a 200 and "Welcome to Dart Frog!".', () { test('responds with a 200 and an empty body.', () async {
final context = _MockRequestContext(); final context = _MockRequestContext();
final response = route.onRequest(context); final response = route.onRequest(context);
final body = await response.body();
expect(response.statusCode, equals(HttpStatus.ok)); expect(response.statusCode, equals(HttpStatus.ok));
expect( expect(
response.body(), body,
completion(equals('Welcome to Dart Frog!')), '',
); );
}); });
}); });

View File

@ -0,0 +1,560 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c"
url: "https://pub.dev"
source: hosted
version: "79.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
adaptive_number:
dependency: transitive
description:
name: adaptive_number
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643
url: "https://pub.dev"
source: hosted
version: "7.2.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
backend:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1+1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.11.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
dart_frog:
dependency: transitive
description:
name: dart_frog
sha256: "569db68a710bcadf96d8addc8988d09a93c4a9521cb6467c2df5ee0ab939c8a4"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
dart_frog_auth:
dependency: transitive
description:
name: dart_frog_auth
sha256: a4c05e6764a82f8997dee9fb68661a473afea92527a2dd8a6677e5a1b454b463
url: "https://pub.dev"
source: hosted
version: "1.2.0"
dart_jsonwebtoken:
dependency: transitive
description:
name: dart_jsonwebtoken
sha256: "00a0812d2aeaeb0d30bcbc4dd3cee57971dbc0ab2216adf4f0247f37793f15ef"
url: "https://pub.dev"
source: hosted
version: "2.17.0"
drift:
dependency: transitive
description:
name: drift
sha256: "76f23535e19a9f2be92f954e74d8802e96f526e5195d7408c1a20f6659043941"
url: "https://pub.dev"
source: hosted
version: "2.24.0"
ed25519_edwards:
dependency: transitive
description:
name: ed25519_edwards
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
url: "https://pub.dev"
source: hosted
version: "4.3.0"
http:
dependency: "direct main"
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_methods:
dependency: transitive
description:
name: http_methods
sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shared_models:
dependency: transitive
description:
path: "../../shared_models"
relative: true
source: path
version: "1.0.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_hotreload:
dependency: transitive
description:
name: shelf_hotreload
sha256: d7099618b18d3c63ba5272491c1812c306629495129ef9996115f0417902f963
url: "https://pub.dev"
source: hosted
version: "1.5.0"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "35d3726fe18ab1463403a5cc8d97dbc81f2a0b08082e8173851363fcc97b6627"
url: "https://pub.dev"
source: hosted
version: "2.7.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct main"
description:
name: test
sha256: "8391fbe68d520daf2314121764d38e37f934c02fd7301ad18307bd93bd6b725d"
url: "https://pub.dev"
source: hosted
version: "1.25.14"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"

View File

@ -0,0 +1,13 @@
name: backend_e2e_tests
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
backend:
path: ..
shared_models:
path: ../../shared_models
test: ^1.24.0
http: ^1.1.0

View File

@ -0,0 +1,64 @@
import 'dart:convert';
import 'package:backend/service/db_access.dart';
import 'package:http/http.dart' as http;
import 'package:shared_models/room.dart';
import 'package:shared_models/user.dart';
import 'package:test/test.dart';
void main() {
const baseUrl = 'http://localhost:8080'; // Adjust as needed
test('Complete room creation and ping flow', () async {
// 1. Create Room
final createRoomResponse = await http.post(
Uri.parse('$baseUrl/create_room'),
);
expect(createRoomResponse.statusCode, 200);
final createRoomData = CreateRoomResponse.fromJson(
json.decode(createRoomResponse.body) as Map<String, dynamic>,
);
expect(createRoomData.success, true);
expect(createRoomData.roomCode, isNotNull);
final roomCode = createRoomData.roomCode!;
// 2. Join Room
final joinRoomResponse = await http.post(
Uri.parse('$baseUrl/join_room'),
body: jsonEncode(JoinRoomRequest(username: 'testUser123', roomCode: createRoomData.roomCode!).toJson()),
headers: {'Content-Type': 'application/json'},
);
expect(joinRoomResponse.statusCode, 200);
final joinRoomData = JoinRoomResponse.fromJson(
json.decode(joinRoomResponse.body) as Map<String, dynamic>,
);
expect(joinRoomData.success, true);
expect(joinRoomData.token, isNotNull);
expect(joinRoomData.uuid, isNotNull);
final token = joinRoomData.token!;
// 3. Ping Room
final pingResponse = await http.get(
Uri.parse('$baseUrl/room/$roomCode/ping'),
headers: {
'Authorization': 'Bearer $token',
},
);
expect(pingResponse.statusCode, 200);
final testUser = await Db.getUserById(joinRoomData.uuid!);
expect(testUser, isNotNull);
expect(testUser!.uuid, joinRoomData.uuid);
final gameRoom = await Db.getRoomByCode(createRoomData.roomCode);
expect(gameRoom, isNotNull);
expect(gameRoom!.code, createRoomData.roomCode);
});
}

View File

@ -320,6 +320,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.0" version: "4.3.0"
http:
dependency: transitive
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:

View File

@ -0,0 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
part 'jwt.g.dart';
@JsonSerializable()
class JWTBody {
String uuid;
String roomUuid;
int iat;
int exp;
JWTBody({required this.uuid, required this.roomUuid, required this.iat, required this.exp});
factory JWTBody.fromJson(Map<String, dynamic> json) => _$JWTBodyFromJson(json);
Map<String, dynamic> toJson() => _$JWTBodyToJson(this);
}

View File

@ -2,6 +2,8 @@ import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; part 'user.g.dart';
typedef JoinRoomRequest = CreateUserRequest;
@JsonSerializable() @JsonSerializable()
class CreateUserRequest { class CreateUserRequest {
final String username; final String username;
@ -13,13 +15,16 @@ class CreateUserRequest {
Map<String, dynamic> toJson() => _$CreateUserRequestToJson(this); Map<String, dynamic> toJson() => _$CreateUserRequestToJson(this);
} }
typedef JoinRoomResponse = CreateUserResponse;
@JsonSerializable() @JsonSerializable()
class CreateUserResponse { class CreateUserResponse {
final String? token; final String? token;
final String? error; final String? error;
final String? uuid;
final bool success; final bool success;
CreateUserResponse({required this.token, required this.success, this.error}); CreateUserResponse({required this.token, required this.uuid, required this.success, this.error});
factory CreateUserResponse.fromJson(Map<String, dynamic> json) => _$CreateUserResponseFromJson(json); factory CreateUserResponse.fromJson(Map<String, dynamic> json) => _$CreateUserResponseFromJson(json);