mirror of https://github.com/flutter/pinball.git
feat: add leaderboard repository (#54)
* feat: add leaderboard repository * refactor: move leaderboard ranking to models * refactor: username to playerInitials * docs: typo fix * docs: adjust repo wording * fix: failing testpull/56/head
parent
aba49660c2
commit
c0175b0d30
@ -0,0 +1,18 @@
|
|||||||
|
name: leaderboard_repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "packages/leaderboard_repository/**"
|
||||||
|
- ".github/workflows/leaderboard_repository.yaml"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "packages/leaderboard_repository/**"
|
||||||
|
- ".github/workflows/leaderboard_repository.yaml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
|
||||||
|
with:
|
||||||
|
working_directory: packages/leaderboard_repository
|
@ -0,0 +1,39 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# VSCode related
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Web related
|
||||||
|
lib/generated_plugin_registrant.dart
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
@ -0,0 +1,11 @@
|
|||||||
|
# leaderboard_repository
|
||||||
|
|
||||||
|
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
|
||||||
|
[![License: MIT][license_badge]][license_link]
|
||||||
|
|
||||||
|
Repository to access leaderboard data in Firebase Cloud Firestore.
|
||||||
|
|
||||||
|
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||||
|
[license_link]: https://opensource.org/licenses/MIT
|
||||||
|
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
|
||||||
|
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
|
@ -0,0 +1 @@
|
|||||||
|
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
@ -0,0 +1,4 @@
|
|||||||
|
library leaderboard_repository;
|
||||||
|
|
||||||
|
export 'src/leaderboard_repository.dart';
|
||||||
|
export 'src/models/models.dart';
|
@ -0,0 +1,152 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
|
||||||
|
/// {@template leaderboard_exception}
|
||||||
|
/// Base exception for leaderboard repository failures.
|
||||||
|
/// {@endtemplate}
|
||||||
|
abstract class LeaderboardException implements Exception {
|
||||||
|
/// {@macro leaderboard_exception}
|
||||||
|
const LeaderboardException(this.error, this.stackTrace);
|
||||||
|
|
||||||
|
/// The error that was caught.
|
||||||
|
final Object error;
|
||||||
|
|
||||||
|
/// The Stacktrace associated with the [error].
|
||||||
|
final StackTrace stackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template leaderboard_deserialization_exception}
|
||||||
|
/// Exception thrown when leaderboard data cannot be deserialized in the
|
||||||
|
/// expected way.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class LeaderboardDeserializationException extends LeaderboardException {
|
||||||
|
/// {@macro leaderboard_deserialization_exception}
|
||||||
|
const LeaderboardDeserializationException(
|
||||||
|
Object error,
|
||||||
|
StackTrace stackTrace,
|
||||||
|
) : super(
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template fetch_top_10_leaderboard_exception}
|
||||||
|
/// Exception thrown when failure occurs while fetching top 10 leaderboard.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FetchTop10LeaderboardException extends LeaderboardException {
|
||||||
|
/// {@macro fetch_top_10_leaderboard_exception}
|
||||||
|
const FetchTop10LeaderboardException(
|
||||||
|
Object error,
|
||||||
|
StackTrace stackTrace,
|
||||||
|
) : super(
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template add_leaderboard_entry_exception}
|
||||||
|
/// Exception thrown when failure occurs while adding entry to leaderboard.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AddLeaderboardEntryException extends LeaderboardException {
|
||||||
|
/// {@macro add_leaderboard_entry_exception}
|
||||||
|
const AddLeaderboardEntryException(
|
||||||
|
Object error,
|
||||||
|
StackTrace stackTrace,
|
||||||
|
) : super(
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template fetch_player_ranking_exception}
|
||||||
|
/// Exception thrown when failure occurs while fetching player ranking.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FetchPlayerRankingException extends LeaderboardException {
|
||||||
|
/// {@macro fetch_player_ranking_exception}
|
||||||
|
const FetchPlayerRankingException(
|
||||||
|
Object error,
|
||||||
|
StackTrace stackTrace,
|
||||||
|
) : super(
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template leaderboard_repository}
|
||||||
|
/// Repository to access leaderboard data in Firebase Cloud Firestore.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class LeaderboardRepository {
|
||||||
|
/// {@macro leaderboard_repository}
|
||||||
|
const LeaderboardRepository(
|
||||||
|
FirebaseFirestore firebaseFirestore,
|
||||||
|
) : _firebaseFirestore = firebaseFirestore;
|
||||||
|
|
||||||
|
final FirebaseFirestore _firebaseFirestore;
|
||||||
|
|
||||||
|
/// Acquires top 10 [LeaderboardEntry]s.
|
||||||
|
Future<List<LeaderboardEntry>> fetchTop10Leaderboard() async {
|
||||||
|
final leaderboardEntries = <LeaderboardEntry>[];
|
||||||
|
late List<QueryDocumentSnapshot> documents;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final querySnapshot = await _firebaseFirestore
|
||||||
|
.collection('leaderboard')
|
||||||
|
.orderBy('score')
|
||||||
|
.limit(10)
|
||||||
|
.get();
|
||||||
|
documents = querySnapshot.docs;
|
||||||
|
} on Exception catch (error, stackTrace) {
|
||||||
|
throw FetchTop10LeaderboardException(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final document in documents) {
|
||||||
|
final data = document.data() as Map<String, dynamic>?;
|
||||||
|
if (data != null) {
|
||||||
|
try {
|
||||||
|
leaderboardEntries.add(LeaderboardEntry.fromJson(data));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
throw LeaderboardDeserializationException(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return leaderboardEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds player's score entry to the leaderboard and gets their
|
||||||
|
/// [LeaderboardRanking].
|
||||||
|
Future<LeaderboardRanking> addLeaderboardEntry(LeaderboardEntry entry) async {
|
||||||
|
late DocumentReference entryReference;
|
||||||
|
try {
|
||||||
|
entryReference = await _firebaseFirestore
|
||||||
|
.collection('leaderboard')
|
||||||
|
.add(entry.toJson());
|
||||||
|
} on Exception catch (error, stackTrace) {
|
||||||
|
throw AddLeaderboardEntryException(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final querySnapshot = await _firebaseFirestore
|
||||||
|
.collection('leaderboard')
|
||||||
|
.orderBy('score')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final documents = querySnapshot.docs;
|
||||||
|
final ranking = documents.indexWhere(
|
||||||
|
(document) => document.id == entryReference.id,
|
||||||
|
) +
|
||||||
|
1;
|
||||||
|
|
||||||
|
if (ranking > 0) {
|
||||||
|
return LeaderboardRanking(ranking: ranking, outOf: documents.length);
|
||||||
|
} else {
|
||||||
|
throw FetchPlayerRankingException(
|
||||||
|
'Player score could not be found and ranking cannot be provided.',
|
||||||
|
StackTrace.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on Exception catch (error, stackTrace) {
|
||||||
|
throw FetchPlayerRankingException(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'leaderboard_entry.g.dart';
|
||||||
|
|
||||||
|
/// Google character type associated with a [LeaderboardEntry].
|
||||||
|
enum CharacterType {
|
||||||
|
/// Dash character.
|
||||||
|
dash,
|
||||||
|
|
||||||
|
/// Sparky character.
|
||||||
|
|
||||||
|
sparky,
|
||||||
|
|
||||||
|
/// Android character.
|
||||||
|
|
||||||
|
android,
|
||||||
|
|
||||||
|
/// Dino character.
|
||||||
|
|
||||||
|
dino,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template leaderboard_entry}
|
||||||
|
/// A model representing a leaderboard entry containing the player's initials,
|
||||||
|
/// score, and chosen character.
|
||||||
|
///
|
||||||
|
/// Stored in Firestore `leaderboard` collection.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "playerInitials" : "ABC",
|
||||||
|
/// "score" : 1500,
|
||||||
|
/// "character" : "dash"
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@endtemplate}
|
||||||
|
@JsonSerializable()
|
||||||
|
class LeaderboardEntry extends Equatable {
|
||||||
|
/// {@macro leaderboard_entry}
|
||||||
|
const LeaderboardEntry({
|
||||||
|
required this.playerInitials,
|
||||||
|
required this.score,
|
||||||
|
required this.character,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Factory which converts a [Map] into a [LeaderboardEntry].
|
||||||
|
factory LeaderboardEntry.fromJson(Map<String, dynamic> json) {
|
||||||
|
return _$LeaderboardEntryFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the [LeaderboardEntry] to [Map].
|
||||||
|
Map<String, dynamic> toJson() => _$LeaderboardEntryToJson(this);
|
||||||
|
|
||||||
|
/// Player's chosen initials for [LeaderboardEntry].
|
||||||
|
///
|
||||||
|
/// Example: 'ABC'.
|
||||||
|
@JsonKey(name: 'playerInitials')
|
||||||
|
final String playerInitials;
|
||||||
|
|
||||||
|
/// Score for [LeaderboardEntry].
|
||||||
|
///
|
||||||
|
/// Example: 1500.
|
||||||
|
@JsonKey(name: 'score')
|
||||||
|
final int score;
|
||||||
|
|
||||||
|
/// [CharacterType] for [LeaderboardEntry].
|
||||||
|
///
|
||||||
|
/// Example: [CharacterType.dash].
|
||||||
|
@JsonKey(name: 'character')
|
||||||
|
final CharacterType character;
|
||||||
|
|
||||||
|
/// An empty [LeaderboardEntry] object.
|
||||||
|
static const empty = LeaderboardEntry(
|
||||||
|
playerInitials: '',
|
||||||
|
score: 0,
|
||||||
|
character: CharacterType.dash,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [playerInitials, score, character];
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'leaderboard_entry.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
LeaderboardEntry _$LeaderboardEntryFromJson(Map<String, dynamic> json) =>
|
||||||
|
LeaderboardEntry(
|
||||||
|
playerInitials: json['playerInitials'] as String,
|
||||||
|
score: json['score'] as int,
|
||||||
|
character: $enumDecode(_$CharacterTypeEnumMap, json['character']),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LeaderboardEntryToJson(LeaderboardEntry instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'playerInitials': instance.playerInitials,
|
||||||
|
'score': instance.score,
|
||||||
|
'character': _$CharacterTypeEnumMap[instance.character],
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$CharacterTypeEnumMap = {
|
||||||
|
CharacterType.dash: 'dash',
|
||||||
|
CharacterType.sparky: 'sparky',
|
||||||
|
CharacterType.android: 'android',
|
||||||
|
CharacterType.dino: 'dino',
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
|
||||||
|
/// {@template leaderboard_ranking}
|
||||||
|
/// Contains [ranking] for a single [LeaderboardEntry] and the number of players
|
||||||
|
/// the [ranking] is [outOf].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class LeaderboardRanking extends Equatable {
|
||||||
|
/// {@macro leaderboard_ranking}
|
||||||
|
const LeaderboardRanking({required this.ranking, required this.outOf});
|
||||||
|
|
||||||
|
/// Place ranking by score for a [LeaderboardEntry].
|
||||||
|
final int ranking;
|
||||||
|
|
||||||
|
/// Number of [LeaderboardEntry]s at the time of score entry.
|
||||||
|
final int outOf;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [ranking, outOf];
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export 'leaderboard_entry.dart';
|
||||||
|
export 'leaderboard_ranking.dart';
|
@ -0,0 +1,23 @@
|
|||||||
|
name: leaderboard_repository
|
||||||
|
description: Repository to access leaderboard data in Firebase Cloud Firestore.
|
||||||
|
version: 1.0.0+1
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.16.0 <3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
cloud_firestore: ^3.1.10
|
||||||
|
equatable: ^2.0.3
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
json_annotation: ^4.4.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
build_runner: ^2.1.8
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
json_serializable: ^6.1.5
|
||||||
|
mocktail: ^0.2.0
|
||||||
|
test: ^1.19.2
|
||||||
|
very_good_analysis: ^2.4.0
|
@ -0,0 +1,226 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors, subtype_of_sealed_class
|
||||||
|
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class MockFirebaseFirestore extends Mock implements FirebaseFirestore {}
|
||||||
|
|
||||||
|
class MockCollectionReference extends Mock
|
||||||
|
implements CollectionReference<Map<String, dynamic>> {}
|
||||||
|
|
||||||
|
class MockQuery extends Mock implements Query<Map<String, dynamic>> {}
|
||||||
|
|
||||||
|
class MockQuerySnapshot extends Mock
|
||||||
|
implements QuerySnapshot<Map<String, dynamic>> {}
|
||||||
|
|
||||||
|
class MockQueryDocumentSnapshot extends Mock
|
||||||
|
implements QueryDocumentSnapshot<Map<String, dynamic>> {}
|
||||||
|
|
||||||
|
class MockDocumentReference extends Mock
|
||||||
|
implements DocumentReference<Map<String, dynamic>> {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('LeaderboardRepository', () {
|
||||||
|
late FirebaseFirestore firestore;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
firestore = MockFirebaseFirestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(LeaderboardRepository(firestore), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('fetchTop10Leaderboard', () {
|
||||||
|
late LeaderboardRepository leaderboardRepository;
|
||||||
|
late CollectionReference<Map<String, dynamic>> collectionReference;
|
||||||
|
late Query<Map<String, dynamic>> query;
|
||||||
|
late QuerySnapshot<Map<String, dynamic>> querySnapshot;
|
||||||
|
late List<QueryDocumentSnapshot<Map<String, dynamic>>>
|
||||||
|
queryDocumentSnapshots;
|
||||||
|
|
||||||
|
final top10Scores = [
|
||||||
|
2500,
|
||||||
|
2200,
|
||||||
|
2200,
|
||||||
|
2000,
|
||||||
|
1800,
|
||||||
|
1400,
|
||||||
|
1300,
|
||||||
|
1000,
|
||||||
|
600,
|
||||||
|
300,
|
||||||
|
100,
|
||||||
|
];
|
||||||
|
|
||||||
|
final top10Leaderboard = top10Scores
|
||||||
|
.map(
|
||||||
|
(score) => LeaderboardEntry(
|
||||||
|
playerInitials: 'user$score',
|
||||||
|
score: score,
|
||||||
|
character: CharacterType.dash,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
leaderboardRepository = LeaderboardRepository(firestore);
|
||||||
|
collectionReference = MockCollectionReference();
|
||||||
|
query = MockQuery();
|
||||||
|
querySnapshot = MockQuerySnapshot();
|
||||||
|
queryDocumentSnapshots = top10Scores.map((score) {
|
||||||
|
final queryDocumentSnapshot = MockQueryDocumentSnapshot();
|
||||||
|
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
|
||||||
|
'character': 'dash',
|
||||||
|
'playerInitials': 'user$score',
|
||||||
|
'score': score
|
||||||
|
});
|
||||||
|
return queryDocumentSnapshot;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
when(() => firestore.collection('leaderboard'))
|
||||||
|
.thenAnswer((_) => collectionReference);
|
||||||
|
when(() => collectionReference.orderBy('score'))
|
||||||
|
.thenAnswer((_) => query);
|
||||||
|
when(() => query.limit(10)).thenAnswer((_) => query);
|
||||||
|
when(query.get).thenAnswer((_) async => querySnapshot);
|
||||||
|
when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'returns top 10 entries when '
|
||||||
|
'retrieving information from firestore succeeds', () async {
|
||||||
|
final top10LeaderboardResults =
|
||||||
|
await leaderboardRepository.fetchTop10Leaderboard();
|
||||||
|
|
||||||
|
expect(top10LeaderboardResults, equals(top10Leaderboard));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws FetchTop10LeaderboardException when Exception occurs '
|
||||||
|
'when trying to retrieve information from firestore', () async {
|
||||||
|
when(() => firestore.collection('leaderboard')).thenThrow(Exception());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => leaderboardRepository.fetchTop10Leaderboard(),
|
||||||
|
throwsA(isA<FetchTop10LeaderboardException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws LeaderboardDeserializationException when Exception occurs '
|
||||||
|
'during deserialization', () async {
|
||||||
|
final top10LeaderboardDataMalformed = <String, dynamic>{
|
||||||
|
'playerInitials': 'ABC',
|
||||||
|
'score': 1500,
|
||||||
|
};
|
||||||
|
final queryDocumentSnapshot = MockQueryDocumentSnapshot();
|
||||||
|
when(() => querySnapshot.docs).thenReturn([queryDocumentSnapshot]);
|
||||||
|
when(queryDocumentSnapshot.data)
|
||||||
|
.thenReturn(top10LeaderboardDataMalformed);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => leaderboardRepository.fetchTop10Leaderboard(),
|
||||||
|
throwsA(isA<LeaderboardDeserializationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('addLeaderboardEntry', () {
|
||||||
|
late LeaderboardRepository leaderboardRepository;
|
||||||
|
late CollectionReference<Map<String, dynamic>> collectionReference;
|
||||||
|
late DocumentReference<Map<String, dynamic>> documentReference;
|
||||||
|
late Query<Map<String, dynamic>> query;
|
||||||
|
late QuerySnapshot<Map<String, dynamic>> querySnapshot;
|
||||||
|
late List<QueryDocumentSnapshot<Map<String, dynamic>>>
|
||||||
|
queryDocumentSnapshots;
|
||||||
|
|
||||||
|
const entryScore = 1500;
|
||||||
|
final leaderboardScores = [
|
||||||
|
2500,
|
||||||
|
2200,
|
||||||
|
entryScore,
|
||||||
|
1000,
|
||||||
|
];
|
||||||
|
final leaderboardEntry = LeaderboardEntry(
|
||||||
|
playerInitials: 'ABC',
|
||||||
|
score: entryScore,
|
||||||
|
character: CharacterType.dash,
|
||||||
|
);
|
||||||
|
const entryDocumentId = 'id$entryScore';
|
||||||
|
final ranking = LeaderboardRanking(ranking: 3, outOf: 4);
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
leaderboardRepository = LeaderboardRepository(firestore);
|
||||||
|
collectionReference = MockCollectionReference();
|
||||||
|
documentReference = MockDocumentReference();
|
||||||
|
query = MockQuery();
|
||||||
|
querySnapshot = MockQuerySnapshot();
|
||||||
|
queryDocumentSnapshots = leaderboardScores.map((score) {
|
||||||
|
final queryDocumentSnapshot = MockQueryDocumentSnapshot();
|
||||||
|
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
|
||||||
|
'character': 'dash',
|
||||||
|
'username': 'user$score',
|
||||||
|
'score': score
|
||||||
|
});
|
||||||
|
when(() => queryDocumentSnapshot.id).thenReturn('id$score');
|
||||||
|
return queryDocumentSnapshot;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
when(() => firestore.collection('leaderboard'))
|
||||||
|
.thenAnswer((_) => collectionReference);
|
||||||
|
when(() => collectionReference.add(any()))
|
||||||
|
.thenAnswer((_) async => documentReference);
|
||||||
|
when(() => collectionReference.orderBy('score'))
|
||||||
|
.thenAnswer((_) => query);
|
||||||
|
when(query.get).thenAnswer((_) async => querySnapshot);
|
||||||
|
when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots);
|
||||||
|
when(() => documentReference.id).thenReturn(entryDocumentId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'adds leaderboard entry and returns player ranking when '
|
||||||
|
'firestore operations succeed', () async {
|
||||||
|
final rankingResult =
|
||||||
|
await leaderboardRepository.addLeaderboardEntry(leaderboardEntry);
|
||||||
|
|
||||||
|
expect(rankingResult, equals(ranking));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws AddLeaderboardEntryException when Exception occurs '
|
||||||
|
'when trying to add entry to firestore', () async {
|
||||||
|
when(() => firestore.collection('leaderboard')).thenThrow(Exception());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry),
|
||||||
|
throwsA(isA<AddLeaderboardEntryException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws FetchPlayerRankingException when Exception occurs '
|
||||||
|
'when trying to retrieve information from firestore', () async {
|
||||||
|
when(() => collectionReference.orderBy('score')).thenThrow(Exception());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry),
|
||||||
|
throwsA(isA<FetchPlayerRankingException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws FetchPlayerRankingException when score cannot be found '
|
||||||
|
'in firestore leaderboard data', () async {
|
||||||
|
when(() => documentReference.id).thenReturn('nonexistentDocumentId');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry),
|
||||||
|
throwsA(isA<FetchPlayerRankingException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('LeaderboardEntry', () {
|
||||||
|
const data = <String, dynamic>{
|
||||||
|
'playerInitials': 'ABC',
|
||||||
|
'score': 1500,
|
||||||
|
'character': 'dash',
|
||||||
|
};
|
||||||
|
|
||||||
|
const leaderboardEntry = LeaderboardEntry(
|
||||||
|
playerInitials: 'ABC',
|
||||||
|
score: 1500,
|
||||||
|
character: CharacterType.dash,
|
||||||
|
);
|
||||||
|
|
||||||
|
test('can be instantiated', () {
|
||||||
|
const leaderboardEntry = LeaderboardEntry.empty;
|
||||||
|
|
||||||
|
expect(leaderboardEntry, isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports value equality.', () {
|
||||||
|
const leaderboardEntry = LeaderboardEntry.empty;
|
||||||
|
const leaderboardEntry2 = LeaderboardEntry.empty;
|
||||||
|
|
||||||
|
expect(leaderboardEntry, equals(leaderboardEntry2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be converted to json', () {
|
||||||
|
expect(leaderboardEntry.toJson(), equals(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be obtained from json', () {
|
||||||
|
final leaderboardEntryFrom = LeaderboardEntry.fromJson(data);
|
||||||
|
|
||||||
|
expect(leaderboardEntry, equals(leaderboardEntryFrom));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('LeaderboardRanking', () {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1);
|
||||||
|
|
||||||
|
expect(leaderboardRanking, isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports value equality.', () {
|
||||||
|
const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1);
|
||||||
|
const leaderboardRanking2 = LeaderboardRanking(ranking: 1, outOf: 1);
|
||||||
|
|
||||||
|
expect(leaderboardRanking, equals(leaderboardRanking2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue