Skip to content

Commit

Permalink
Merge branch 'main' into feat/board-full-render
Browse files Browse the repository at this point in the history
  • Loading branch information
erickzanardo authored Mar 8, 2024
2 parents cc7414a + 3e93019 commit accea8f
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy_api_staging.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: deploy_api_prod
name: deploy_api_staging

on: workflow_dispatch

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy_app_staging.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: deploy_app_dev
name: deploy_app_staging

on: workflow_dispatch

Expand Down
2 changes: 2 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ COPY packages/db_client ./packages/db_client
COPY packages/encryption_middleware ./packages/encryption_middleware
COPY packages/game_domain ./packages/game_domain
COPY packages/jwt_middleware ./packages/jwt_middleware
COPY packages/leaderboard_repository ./packages/leaderboard_repository
COPY packages/board_renderer ./packages/board_renderer
COPY packages/crossword_repository ./packages/crossword_repository

Expand All @@ -17,6 +18,7 @@ RUN dart pub get -C packages/db_client
RUN dart pub get -C packages/encryption_middleware
RUN dart pub get -C packages/game_domain
RUN dart pub get -C packages/jwt_middleware
RUN dart pub get -C packages/leaderboard_repository

# Resolve app dependencies.
COPY pubspec.* ./
Expand Down
Binary file modified assets/images/letters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/api_client/lib/src/api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';

import 'package:api_client/api_client.dart';
import 'package:api_client/src/resources/leaderboard_resource.dart';
import 'package:http/http.dart' as http;

/// {@template api_client}
Expand Down Expand Up @@ -51,6 +52,10 @@ class ApiClient {
if (_appCheckToken != null) 'X-Firebase-AppCheck': _appCheckToken!,
};

/// {@macro leaderboard_resource}
late final LeaderboardResource leaderboardResource =
LeaderboardResource(apiClient: this);

Future<http.Response> _handleUnauthorized(
Future<http.Response> Function() sendRequest,
) async {
Expand Down
98 changes: 98 additions & 0 deletions packages/api_client/lib/src/resources/leaderboard_resource.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'dart:convert';
import 'dart:io';

import 'package:api_client/api_client.dart';
import 'package:game_domain/game_domain.dart';

/// {@template leaderboard_resource}
/// An api resource for interacting with the leaderboard.
/// {@endtemplate}
class LeaderboardResource {
/// {@macro leaderboard_resource}
LeaderboardResource({
required ApiClient apiClient,
}) : _apiClient = apiClient;

final ApiClient _apiClient;

/// Get /game/leaderboard/results
///
/// Returns a list of [LeaderboardPlayer].
Future<List<LeaderboardPlayer>> getLeaderboardResults() async {
final response = await _apiClient.get('/game/leaderboard/results');

if (response.statusCode != HttpStatus.ok) {
throw ApiClientError(
'GET /leaderboard/results returned status ${response.statusCode} '
'with the following response: "${response.body}"',
StackTrace.current,
);
}

try {
final json = jsonDecode(response.body) as Map<String, dynamic>;
final leaderboardPlayers = json['leaderboardPlayers'] as List;

return leaderboardPlayers
.map(
(json) => LeaderboardPlayer.fromJson(json as Map<String, dynamic>),
)
.toList();
} catch (error, stackTrace) {
throw ApiClientError(
'GET /leaderboard/results returned invalid response "${response.body}"',
stackTrace,
);
}
}

/// Get /game/leaderboard/initials_blacklist
///
/// Returns a [List<String>].
Future<List<String>> getInitialsBlacklist() async {
final response =
await _apiClient.get('/game/leaderboard/initials_blacklist');

if (response.statusCode == HttpStatus.notFound) {
return [];
}

if (response.statusCode != HttpStatus.ok) {
throw ApiClientError(
'GET /leaderboard/initials_blacklist returned status '
'${response.statusCode} with the following response: '
'"${response.body}"',
StackTrace.current,
);
}

try {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return (json['list'] as List).cast<String>();
} catch (error, stackTrace) {
throw ApiClientError(
'GET /leaderboard/initials_blacklist '
'returned invalid response "${response.body}"',
stackTrace,
);
}
}

/// Post /game/leaderboard/initials
Future<void> addLeaderboardPlayer({
required LeaderboardPlayer leaderboardPlayer,
}) async {
final response = await _apiClient.post(
'/game/leaderboard/initials',
body: jsonEncode(leaderboardPlayer.toJson()),
);

if (response.statusCode != HttpStatus.noContent) {
throw ApiClientError(
'POST /leaderboard/initials returned status ${response.statusCode} '
'with the following response: "${response.body}"',
StackTrace.current,
);
}
}
}
4 changes: 3 additions & 1 deletion packages/api_client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ dev_dependencies:
dependencies:
encrypt: ^5.0.3
equatable: ^2.0.5
http: ^1.2.1
game_domain:
path: ../../api/packages/game_domain
http: ^1.2.1
204 changes: 204 additions & 0 deletions packages/api_client/test/src/resources/leaderboard_resource_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// ignore_for_file: prefer_const_constructors

import 'dart:convert';
import 'dart:io';

import 'package:api_client/api_client.dart';
import 'package:api_client/src/resources/leaderboard_resource.dart';
import 'package:game_domain/game_domain.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class _MockApiClient extends Mock implements ApiClient {}

class _MockResponse extends Mock implements http.Response {}

void main() {
group('LeaderboardResource', () {
late ApiClient apiClient;
late http.Response response;
late LeaderboardResource resource;

setUp(() {
apiClient = _MockApiClient();
response = _MockResponse();

resource = LeaderboardResource(apiClient: apiClient);
});

group('getLeaderboardResults', () {
setUp(() {
when(() => apiClient.get(any())).thenAnswer((_) async => response);
});

test('makes the correct call ', () async {
final leaderboardPlayer = LeaderboardPlayer(
userId: 'id',
score: 10,
initials: 'TST',
);

when(() => response.statusCode).thenReturn(HttpStatus.ok);
when(() => response.body).thenReturn(
jsonEncode(
{
'leaderboardPlayers': [leaderboardPlayer.toJson()],
},
),
);

final results = await resource.getLeaderboardResults();

expect(results, equals([leaderboardPlayer]));
});

test('throws ApiClientError when request fails', () async {
when(() => response.statusCode)
.thenReturn(HttpStatus.internalServerError);
when(() => response.body).thenReturn('Oops');

await expectLater(
resource.getLeaderboardResults,
throwsA(
isA<ApiClientError>().having(
(e) => e.cause,
'cause',
equals(
'GET /leaderboard/results returned status 500 with the following response: "Oops"',
),
),
),
);
});

test('throws ApiClientError when request response is invalid', () async {
when(() => response.statusCode).thenReturn(HttpStatus.ok);
when(() => response.body).thenReturn('Oops');

await expectLater(
resource.getLeaderboardResults,
throwsA(
isA<ApiClientError>().having(
(e) => e.cause,
'cause',
equals(
'GET /leaderboard/results returned invalid response "Oops"',
),
),
),
);
});
});

group('getInitialsBlacklist', () {
setUp(() {
when(() => apiClient.get(any())).thenAnswer((_) async => response);
});

test('gets initials blacklist', () async {
const blacklist = ['WTF'];

when(() => response.statusCode).thenReturn(HttpStatus.ok);
when(() => response.body).thenReturn(jsonEncode({'list': blacklist}));
final result = await resource.getInitialsBlacklist();

expect(result, equals(blacklist));
});

test('gets empty blacklist if endpoint not found', () async {
const emptyList = <String>[];

when(() => response.statusCode).thenReturn(HttpStatus.notFound);
final result = await resource.getInitialsBlacklist();

expect(result, equals(emptyList));
});

test('throws ApiClientError when request fails', () async {
when(() => response.statusCode)
.thenReturn(HttpStatus.internalServerError);
when(() => response.body).thenReturn('Oops');

await expectLater(
resource.getInitialsBlacklist,
throwsA(
isA<ApiClientError>().having(
(e) => e.cause,
'cause',
equals(
'GET /leaderboard/initials_blacklist returned status 500 with the following response: "Oops"',
),
),
),
);
});

test('throws ApiClientError when request response is invalid', () async {
when(() => response.statusCode).thenReturn(HttpStatus.ok);
when(() => response.body).thenReturn('Oops');

await expectLater(
resource.getInitialsBlacklist,
throwsA(
isA<ApiClientError>().having(
(e) => e.cause,
'cause',
equals(
'GET /leaderboard/initials_blacklist returned invalid response "Oops"',
),
),
),
);
});
});

group('addLeaderboardPlayer', () {
final leaderboardPlayer = LeaderboardPlayer(
userId: 'id',
score: 10,
initials: 'TST',
);

setUp(() {
when(() => apiClient.post(any(), body: any(named: 'body')))
.thenAnswer((_) async => response);
});

test('makes the correct call', () async {
when(() => response.statusCode).thenReturn(HttpStatus.noContent);
await resource.addLeaderboardPlayer(
leaderboardPlayer: leaderboardPlayer,
);

verify(
() => apiClient.post(
'/game/leaderboard/initials',
body: jsonEncode(leaderboardPlayer.toJson()),
),
).called(1);
});

test('throws ApiClientError when request fails', () async {
when(() => response.statusCode)
.thenReturn(HttpStatus.internalServerError);
when(() => response.body).thenReturn('Oops');

await expectLater(
() => resource.addLeaderboardPlayer(
leaderboardPlayer: leaderboardPlayer,
),
throwsA(
isA<ApiClientError>().having(
(e) => e.cause,
'cause',
equals(
'POST /leaderboard/initials returned status 500 with the following response: "Oops"',
),
),
),
);
});
});
});
}

0 comments on commit accea8f

Please sign in to comment.