diff --git a/.gitignore b/.gitignore index 3e2ac3f..e75444d 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,9 @@ app.*.map.json # Misc ### +# Dartfrog +**/.dart_frog/** + # Direnv **/.direnv/** diff --git a/.ignore b/.ignore index b4f9b4a..e5c3f70 100644 --- a/.ignore +++ b/.ignore @@ -2,3 +2,7 @@ LICENSE **/.ignore **/.gitignore + +# Dont edit these often, so its just noise in the file picker +backend/test_e2e/pubspec.yaml +backend/scripts/pubspec.yaml diff --git a/README.md b/README.md index 0a64415..98d3c78 100644 --- a/README.md +++ b/README.md @@ -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 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 -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 @@ -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` +### 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/` WIP diff --git a/backend/.dart_frog/server.dart b/backend/.dart_frog/server.dart deleted file mode 100644 index 2c08f7c..0000000 --- a/backend/.dart_frog/server.dart +++ /dev/null @@ -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); -} - diff --git a/backend/analysis_options.yaml b/backend/analysis_options.yaml index 51f78f8..7afe797 100644 --- a/backend/analysis_options.yaml +++ b/backend/analysis_options.yaml @@ -6,6 +6,7 @@ linter: prefer_single_quotes: true public_member_api_docs: false lines_longer_than_80_chars: false + avoid_redundant_argument_values: false analyzer: exclude: diff --git a/backend/lib/authenticator.dart b/backend/lib/authenticator.dart index 5ea1c74..bc52c90 100644 --- a/backend/lib/authenticator.dart +++ b/backend/lib/authenticator.dart @@ -4,6 +4,7 @@ import 'package:backend/database.dart'; import 'package:backend/service/db_access.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:logging/logging.dart'; +import 'package:shared_models/jwt.dart'; import 'package:shared_models/user.dart'; final jwtSecret = _getSecret(); @@ -18,21 +19,16 @@ enum JWTTokenStatus { } 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); - if (newUser == null) return null; + if (newUser == null) return (null, null); final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000; final jwt = JWT( - { - 'uid': newUser.uuid, - 'roomUuid': newUser.gameRoomUuid, - 'iat': iat, - 'exp': iat + expTimeSecs, - }, + JWTBody(uuid: newUser.uuid, roomUuid: newUser.gameRoomUuid, iat: iat, exp: iat + expTimeSecs).toJson(), ); - return jwt.sign(SecretKey(jwtSecret)); + return (jwt.sign(SecretKey(jwtSecret)), newUser); } Future<(User?, JWTTokenStatus)> verifyToken( @@ -45,18 +41,15 @@ class Authenticator { 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; - final exp = payloadData['exp'] as int; - - if (iat + expTimeSecs != exp || DateTime.now().millisecondsSinceEpoch ~/ 1000 > exp) { + if (jwt.iat + expTimeSecs != jwt.exp || DateTime.now().millisecondsSinceEpoch ~/ 1000 > jwt.exp) { return (null, JWTTokenStatus.expired); } - final uuid = payloadData['uuid'] as String; - return (await Db.getUser(uuid), JWTTokenStatus.valid); + return (await Db.getUserById(jwt.uuid), JWTTokenStatus.valid); } catch (e) { + log.fine('Error verifying token', e); return (null, JWTTokenStatus.invalid); } } diff --git a/backend/lib/middleware/auth_middleware.dart b/backend/lib/middleware/auth_middleware.dart index ec0fc44..294f7a0 100644 --- a/backend/lib/middleware/auth_middleware.dart +++ b/backend/lib/middleware/auth_middleware.dart @@ -22,7 +22,7 @@ Middleware tokenAuthMiddleware({ } final auth = context.read<Authenticator>(); // 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(' '); 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}'); @@ -34,7 +34,8 @@ Middleware tokenAuthMiddleware({ if (user == null) { 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); } diff --git a/backend/lib/service/db_access.dart b/backend/lib/service/db_access.dart index b6966d5..b576ff0 100644 --- a/backend/lib/service/db_access.dart +++ b/backend/lib/service/db_access.dart @@ -8,9 +8,9 @@ final log = Logger('Db'); class Db { static final _db = AppDatabase(); - static Future<User> getUser(String uuid) { + static Future<User?> getUserById(String 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 { @@ -32,14 +32,21 @@ class Db { }); } - static Future<GameRoom> createRoom({required String roomCode}) => _db.managers.gameRooms - .createReturning( + static Future<GameRoom?> createRoom({required String roomCode}) => _db.managers.gameRooms + .createReturningOrNull( (o) => o(createdAt: Value(DateTime.now()), status: GameStatus.open, uuid: const Uuid().v4(), code: roomCode), ) .catchError( (Object err) { 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; + } } diff --git a/backend/pubspec.lock b/backend/pubspec.lock index 86881c6..a89e298 100644 --- a/backend/pubspec.lock +++ b/backend/pubspec.lock @@ -302,6 +302,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/backend/pubspec.yaml b/backend/pubspec.yaml index bb327b2..eddc4d0 100644 --- a/backend/pubspec.yaml +++ b/backend/pubspec.yaml @@ -20,6 +20,16 @@ dependencies: dev_dependencies: build_runner: ^2.4.14 drift_dev: ^2.24.0 + # For e2e testing + http: ^1.3.0 mocktail: ^1.0.3 test: ^1.25.5 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 diff --git a/backend/routes/create_room.dart b/backend/routes/create_room.dart index 6ff734c..9ae7641 100644 --- a/backend/routes/create_room.dart +++ b/backend/routes/create_room.dart @@ -6,7 +6,7 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:logging/logging.dart'; import 'package:shared_models/room.dart'; -final log = Logger('create_room'); +final log = Logger('create_room/'); Future<Response> onRequest(RequestContext context) async { // Only allow POST requests @@ -28,6 +28,17 @@ Future<Response> onRequest(RequestContext context) async { // Create the room 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 Response.json( body: CreateRoomResponse(success: true, roomCode: room.code).toJson(), diff --git a/backend/routes/index.dart b/backend/routes/health.dart similarity index 64% rename from backend/routes/index.dart rename to backend/routes/health.dart index a538147..fb0ac8c 100644 --- a/backend/routes/index.dart +++ b/backend/routes/health.dart @@ -1,5 +1,5 @@ import 'package:dart_frog/dart_frog.dart'; Response onRequest(RequestContext context) { - return Response(body: 'Welcome to Dart Frog!'); + return Response(statusCode: 200); } diff --git a/backend/routes/auth/index.dart b/backend/routes/join_room.dart similarity index 64% rename from backend/routes/auth/index.dart rename to backend/routes/join_room.dart index d9cf4e6..d55ddf1 100644 --- a/backend/routes/auth/index.dart +++ b/backend/routes/join_room.dart @@ -5,7 +5,7 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:logging/logging.dart'; import 'package:shared_models/user.dart'; -final log = Logger('auth/'); +final log = Logger('join_room/'); Future<Response> onRequest(RequestContext context) async { // Only allow POST requests @@ -20,33 +20,28 @@ Future<Response> onRequest(RequestContext context) async { // Generate token 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( success: false, 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(); return Response.json( - statusCode: HttpStatus.badRequest, + statusCode: user == null ? HttpStatus.badRequest : HttpStatus.internalServerError, body: body, ); } // Return the token 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) { - log.severe('Error:', e); - final body = CreateUserResponse(success: false, token: null, error: 'Internal server error').toJson(); + log.severe('Error:', e, StackTrace.current); + final body = CreateUserResponse(success: false, token: null, error: 'Internal server error', uuid: null).toJson(); return Response.json( statusCode: HttpStatus.internalServerError, body: body, diff --git a/backend/routes/room/[roomCode]/ping.dart b/backend/routes/room/[roomCode]/ping.dart new file mode 100644 index 0000000..544f9cd --- /dev/null +++ b/backend/routes/room/[roomCode]/ping.dart @@ -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); +} diff --git a/backend/scripts/pubspec.lock b/backend/scripts/pubspec.lock new file mode 100644 index 0000000..2816fe3 --- /dev/null +++ b/backend/scripts/pubspec.lock @@ -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" diff --git a/backend/scripts/pubspec.yaml b/backend/scripts/pubspec.yaml new file mode 100644 index 0000000..9fb8a2d --- /dev/null +++ b/backend/scripts/pubspec.yaml @@ -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 + diff --git a/backend/scripts/run_build_runner.dart b/backend/scripts/run_build_runner.dart new file mode 100644 index 0000000..432c82c --- /dev/null +++ b/backend/scripts/run_build_runner.dart @@ -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); +} diff --git a/backend/scripts/test_runner.dart b/backend/scripts/test_runner.dart new file mode 100644 index 0000000..ccfe4e9 --- /dev/null +++ b/backend/scripts/test_runner.dart @@ -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'); +} diff --git a/backend/test/routes/index_test.dart b/backend/test/routes/health_test.dart similarity index 66% rename from backend/test/routes/index_test.dart rename to backend/test/routes/health_test.dart index 6936e38..44ccd82 100644 --- a/backend/test/routes/index_test.dart +++ b/backend/test/routes/health_test.dart @@ -4,19 +4,20 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; -import '../../routes/index.dart' as route; +import '../../routes/health.dart' as route; class _MockRequestContext extends Mock implements RequestContext {} void main() { - group('GET /', () { - test('responds with a 200 and "Welcome to Dart Frog!".', () { + group('GET /health', () { + test('responds with a 200 and an empty body.', () async { final context = _MockRequestContext(); final response = route.onRequest(context); + final body = await response.body(); expect(response.statusCode, equals(HttpStatus.ok)); expect( - response.body(), - completion(equals('Welcome to Dart Frog!')), + body, + '', ); }); }); diff --git a/backend/test_e2e/pubspec.lock b/backend/test_e2e/pubspec.lock new file mode 100644 index 0000000..7528ee6 --- /dev/null +++ b/backend/test_e2e/pubspec.lock @@ -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" diff --git a/backend/test_e2e/pubspec.yaml b/backend/test_e2e/pubspec.yaml new file mode 100644 index 0000000..cb25b75 --- /dev/null +++ b/backend/test_e2e/pubspec.yaml @@ -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 diff --git a/backend/test_e2e/tests/game_room_test.dart b/backend/test_e2e/tests/game_room_test.dart new file mode 100644 index 0000000..e0ed32e --- /dev/null +++ b/backend/test_e2e/tests/game_room_test.dart @@ -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); + }); +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 18d0ab9..ab1692c 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -320,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/shared_models/lib/jwt.dart b/shared_models/lib/jwt.dart new file mode 100644 index 0000000..1f9291d --- /dev/null +++ b/shared_models/lib/jwt.dart @@ -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); +} diff --git a/shared_models/lib/user.dart b/shared_models/lib/user.dart index a5d7697..a43ea1d 100644 --- a/shared_models/lib/user.dart +++ b/shared_models/lib/user.dart @@ -2,6 +2,8 @@ import 'package:json_annotation/json_annotation.dart'; part 'user.g.dart'; +typedef JoinRoomRequest = CreateUserRequest; + @JsonSerializable() class CreateUserRequest { final String username; @@ -13,13 +15,16 @@ class CreateUserRequest { Map<String, dynamic> toJson() => _$CreateUserRequestToJson(this); } +typedef JoinRoomResponse = CreateUserResponse; + @JsonSerializable() class CreateUserResponse { final String? token; final String? error; + final String? uuid; 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);