Skip to content

Commit

Permalink
feat: adds leaderboard resource (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
AyadLaouissi authored Mar 8, 2024
1 parent 86a233d commit 4dab43d
Show file tree
Hide file tree
Showing 4 changed files with 310 additions and 1 deletion.
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 4dab43d

Please sign in to comment.