Skip to content

Commit

Permalink
ci: add api workflow (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcossevilla authored Nov 28, 2024
1 parent de0234b commit fcccb46
Show file tree
Hide file tree
Showing 27 changed files with 791 additions and 50 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: api

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
pull_request:
paths:
- "api/**"
- ".github/workflows/api.yaml"
branches:
- main

jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1
with:
dart_sdk: 3.5.0
coverage_excludes: "**/*.g.dart"
working_directory: api
analyze_directories: "routes test lib"
report_on: "routes"
4 changes: 2 additions & 2 deletions api/lib/api.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export 'exceptions/exceptions.dart';
export 'extensions/extensions.dart';
export 'src/exceptions/exceptions.dart';
export 'src/extensions/extensions.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class BadRequest implements Exception {
final Map<String, dynamic> data;

/// Converts the [BadRequest] to a JSON-encodable [Map].
Map<String, dynamic> toJson() {
return {'type': type, 'data': data};
}
Map<String, dynamic> toJson() => {'type': type, 'data': data};
}

/// {@template not_found}
Expand All @@ -31,7 +29,5 @@ class NotFound implements Exception {
final Map<String, dynamic> data;

/// Converts the [NotFound] to a JSON-encodable [Map].
Map<String, dynamic> toJson() {
return {'type': type, 'data': data};
}
Map<String, dynamic> toJson() => {'type': type, 'data': data};
}
File renamed without changes.
6 changes: 1 addition & 5 deletions api/packages/db_client/lib/src/models/db_entity_record.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:equatable/equatable.dart';
import 'package:hive_ce/hive.dart';

part 'db_entity_record.g.dart';
Expand All @@ -7,7 +6,7 @@ part 'db_entity_record.g.dart';
/// A model representing a record in an entity.
/// {@endtemplate}
@HiveType(typeId: 0)
class DbEntityRecord extends HiveObject with EquatableMixin {
class DbEntityRecord extends HiveObject {
/// {@macro db_entity_record}
DbEntityRecord({
required this.id,
Expand All @@ -21,7 +20,4 @@ class DbEntityRecord extends HiveObject with EquatableMixin {
/// The record data.
@HiveField(1)
final Map<String, dynamic> data;

@override
List<Object> get props => [id, data];
}
3 changes: 1 addition & 2 deletions api/packages/db_client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ version: 0.1.0+1
publish_to: none

environment:
sdk: ^3.5.0
sdk: ">=3.5.0 <4.0.0"

dependencies:
api_models:
path: ../../../packages/api_models
collection: ^1.19.0
equatable: ^2.0.5
hive_ce: ^2.6.0
json_annotation: ^4.9.0
meta: ^1.16.0
Expand Down
14 changes: 0 additions & 14 deletions api/packages/db_client/test/src/models/db_entity_record_test.dart

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class PassDataSource {
}

/// Reads a single [PkPass] object.
Future<Uint8List> read(String id) async {
Uint8List read(String id) {
try {
final record = _dbClient.get(DataBox.passes, id: id);
final pass = UserPass.fromJson(record.data);
Expand Down
2 changes: 1 addition & 1 deletion api/packages/pass_data_source/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version: 0.1.0+1
publish_to: none

environment:
sdk: ^3.5.0
sdk: ">=3.5.0 <4.0.0"

dependencies:
api_models:
Expand Down
2 changes: 1 addition & 1 deletion api/packages/user_repository/lib/src/user_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class UserRepository {
///
/// The received password should be in plain text, and will be hashed, so it
/// can be compared to the stored password hash.
Future<User?> userFromCredentials(String username, String password) async {
User? userFromCredentials(String username, String password) {
try {
final hashedPassword = _hashValue(password);
final records = _dbClient.getBy(
Expand Down
2 changes: 1 addition & 1 deletion api/packages/user_repository/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version: 0.1.0+1
publish_to: none

environment:
sdk: ^3.5.0
sdk: ">=3.5.0 <4.0.0"

dependencies:
api_models:
Expand Down
8 changes: 1 addition & 7 deletions api/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,17 @@ environment:
dependencies:
api_models:
path: ../packages/api_models
basic_utils: ^5.7.0
collection: ^1.19.0
crypto: ^3.0.5
dart_frog: ^1.0.0
dart_frog_auth: ^1.1.0
db_client:
path: packages/db_client
http: ^1.2.2
meta: ^1.16.0
mime: ^1.0.6
pass_data_source:
path: packages/pass_data_source
path: ^1.9.0
user_repository:
path: packages/user_repository

dev_dependencies:
meta: ^1.16.0
mocktail: ^1.0.0
test: ^1.19.2
very_good_analysis: ^6.0.0
2 changes: 1 addition & 1 deletion api/routes/passes/[id].dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ FutureOr<Response> onRequest(RequestContext context, String id) {
Future<Response> _get(RequestContext context, String id) async {
try {
final passDataSource = await context.readAsync<PassDataSource>();
final pass = await passDataSource.read(id);
final pass = passDataSource.read(id);
return Response.json(
body: {
'data': base64.encode(pass),
Expand Down
20 changes: 18 additions & 2 deletions api/routes/passes/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ FutureOr<Response> onRequest(RequestContext context) {

Future<Response> _get(RequestContext context) async {
final passDataSource = await context.readAsync<PassDataSource>();
final userId = context.request.uri.queryParameters['userId']!;
final userId = context.request.uri.queryParameters['userId'];

if (userId == null) {
return Response(
statusCode: HttpStatus.badRequest,
body: 'Missing query parameter: userId',
);
}

final passes = await passDataSource.readByUserId(userId);
final body = passes.map((pass) => {'data': base64.encode(pass)}).toList();

Expand All @@ -25,7 +33,15 @@ Future<Response> _get(RequestContext context) async {

Future<Response> _post(RequestContext context) async {
final passDataSource = await context.readAsync<PassDataSource>();
final userId = context.request.uri.queryParameters['userId']!;
final userId = context.request.uri.queryParameters['userId'];

if (userId == null) {
return Response(
statusCode: HttpStatus.badRequest,
body: 'Missing query parameter: userId',
);
}

final body = await context.request.body();
final bodyJson = json.decode(body) as Map<String, dynamic>;

Expand Down
2 changes: 1 addition & 1 deletion api/routes/users/_middleware.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Middleware authenticationProvider() {
return basicAuthentication<User>(
authenticator: (context, username, password) async {
final userRepository = await context.readAsync<UserRepository>();
final user = await userRepository.userFromCredentials(username, password);
final user = userRepository.userFromCredentials(username, password);
return user;
},
applies: (context) async {
Expand Down
11 changes: 9 additions & 2 deletions api/routes/users/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ Future<Response> _createUser(RequestContext context) async {
password: password,
);

return Response.json(body: {'id': id});
return Response.json(
statusCode: HttpStatus.created,
body: {'id': id},
);
}

return Response(statusCode: HttpStatus.badRequest);
Expand All @@ -36,7 +39,11 @@ Future<Response> _getUser(RequestContext context) async {
final userRepository = await context.readAsync<UserRepository>();
final queryParams = context.request.uri.queryParameters;

final user = await userRepository.userFromCredentials(
if (queryParams['username'] == null || queryParams['password'] == null) {
return Response(statusCode: HttpStatus.badRequest);
}

final user = userRepository.userFromCredentials(
queryParams['username']!,
queryParams['password']!,
);
Expand Down
8 changes: 8 additions & 0 deletions api/test/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
include: package:very_good_analysis/analysis_options.6.0.0.yaml
linter:
rules:
file_names: false
analyzer:
errors:
prefer_const_constructors: ignore
prefer_const_literals_to_create_immutables: ignore
79 changes: 79 additions & 0 deletions api/test/routes/_middleware_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// ignore_for_file: prefer_const_constructors

import 'dart:io';

import 'package:api/api.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:db_client/db_client.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

import '../../routes/_middleware.dart';

class _MockRequestContext extends Mock implements RequestContext {}

void main() {
group('middleware', () {
late RequestContext context;
late Request request;

setUp(() {
context = _MockRequestContext();
request = Request.get(Uri.parse('http://localhost/'));

when(() => context.request).thenReturn(request);
when(() => context.provide<Future<DbClient>>(any())).thenReturn(context);
});

test('provides a DbClient instance', () async {
final handler = middleware((_) => Response());

await handler(context);

final create = verify(
() => context.provide<Future<DbClient>>(captureAny()),
).captured.single as Future<DbClient> Function();

await expectLater(
create(),
completion(isA<DbClient>()),
);
});

test(
'returns Response with HttpStatus.notFound when NotFound is thrown',
() async {
final handler = middleware((_) => throw NotFound('Not found'));

await expectLater(
handler(context),
completion(
isA<Response>().having(
(response) => response.statusCode,
'statusCode',
equals(HttpStatus.notFound),
),
),
);
},
);

test(
'returns Response with HttpStatus.badRequest when BadRequest is thrown',
() async {
final handler = middleware((_) => throw BadRequest('Bad request'));

await expectLater(
handler(context),
completion(
isA<Response>().having(
(response) => response.statusCode,
'statusCode',
equals(HttpStatus.badRequest),
),
),
);
},
);
});
}
2 changes: 1 addition & 1 deletion api/test/routes/index_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import '../../routes/index.dart' as route;
class _MockRequestContext extends Mock implements RequestContext {}

void main() {
group('GET /', () {
group('[GET]', () {
test('responds with a 200 and "Welcome to Dart Frog!".', () {
final context = _MockRequestContext();
final response = route.onRequest(context);
Expand Down
Loading

0 comments on commit fcccb46

Please sign in to comment.