Skip to content

Commit

Permalink
feat: update answer word endpoint (#311)
Browse files Browse the repository at this point in the history
* feat: upload all answers as their own docs

* feat: update answer endpoint

* test: fix mock

* feat: add error handling
  • Loading branch information
jsgalarraga authored Apr 17, 2024
1 parent 385d9a2 commit 5b63c8b
Show file tree
Hide file tree
Showing 16 changed files with 427 additions and 176 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
library;

export 'src/crossword_repository.dart';
export 'src/crossword_repository_exception.dart';
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:crossword_repository/crossword_repository.dart';
import 'package:db_client/db_client.dart';
import 'package:game_domain/game_domain.dart';

Expand All @@ -15,6 +16,8 @@ class CrosswordRepository {
final DbClient _dbClient;

static const _sectionsCollection = 'boardChunks';
static const _answersCollection = 'answers';
static const _boardInfoCollection = 'boardInfo';

/// Fetches all sections from the board.
Future<List<BoardSection>> listAllSections() async {
Expand Down Expand Up @@ -59,33 +62,50 @@ class CrosswordRepository {
);
}

/// Fetches a word answer by its id.
Future<String?> findAnswerById(String id) async {
final result = await _dbClient.getById(_answersCollection, 'id$id');

if (result != null) {
return result.data['answer'] as String;
}

return null;
}

/// Tries solving a word.
/// Returns true if succeeds and updates the word's solvedTimestamp
/// attribute.
Future<bool> answerWord(
int sectionX,
int sectionY,
int wordX,
int wordY,
String wordId,
Mascots mascot,
String answer,
String userAnswer,
) async {
final section = await findSectionByPosition(sectionX, sectionY);

if (section == null) {
return false;
throw CrosswordRepositoryException(
'Section not found for position ($sectionX, $sectionY)',
StackTrace.current,
);
}

final word = section.words.firstWhereOrNull(
(element) => element.position.x == wordX && element.position.y == wordY,
);
final word = section.words.firstWhereOrNull((e) => e.id == wordId);

if (word == null) {
return false;
throw CrosswordRepositoryException(
'Word with id $wordId not found for section ($sectionX, $sectionY)',
StackTrace.current,
);
}

if (answer == word.answer) {
final correctAnswer = await findAnswerById(wordId);

if (userAnswer.toLowerCase() == correctAnswer?.toLowerCase()) {
final solvedWord = word.copyWith(
answer: correctAnswer,
solvedTimestamp: clock.now().millisecondsSinceEpoch,
mascot: mascot,
);
Expand All @@ -97,4 +117,28 @@ class CrosswordRepository {
}
return false;
}

/// Adds one to the solved words count in the crossword.
Future<void> updateSolvedWordsCount() async {
final snapshot = await _dbClient.find(
_boardInfoCollection,
{
'type': 'solved_words_count',
},
);

final document = snapshot.first;
final solvedWordsCount = document.data['value'] as int;
final newValue = solvedWordsCount + 1;

await _dbClient.update(
_boardInfoCollection,
DbEntityRecord(
id: document.id,
data: {
'value': newValue,
},
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:crossword_repository/crossword_repository.dart';

/// {@template crossword_repository_exception}
/// Exception thrown when an error occurs in the [CrosswordRepository].
/// {@endtemplate}
class CrosswordRepositoryException implements Exception {
/// {@macro crossword_repository_exception}
CrosswordRepositoryException(this.cause, this.stackTrace);

/// Error cause.
final dynamic cause;

/// The stack trace of the error.
final StackTrace stackTrace;

@override
String toString() {
return '''
cause: $cause
stackTrace: $stackTrace
''';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:crossword_repository/crossword_repository.dart';
import 'package:test/test.dart';

void main() {
group('CrosswordRepositoryException', () {
test('can be converted to a string', () {
final exception = CrosswordRepositoryException(
'something is broken',
StackTrace.fromString('it happened here'),
);

expect(
exception.toString(),
equals('''
cause: something is broken
stackTrace: it happened here
'''),
);
});
});
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// ignore_for_file: prefer_const_constructors
// ignore_for_file: prefer_const_literals_to_create_immutables

import 'package:clock/clock.dart';
import 'package:crossword_repository/crossword_repository.dart';
import 'package:db_client/db_client.dart';
Expand All @@ -15,6 +17,7 @@ void main() {
late DbClient dbClient;

const sectionsCollection = 'boardChunks';
const answersCollection = 'answers';

setUpAll(() {
registerFallbackValue(_MockDbEntityRecord());
Expand Down Expand Up @@ -160,38 +163,41 @@ void main() {
'position': {'x': 1, 'y': 1},
'size': 300,
'words': [
{
'id': '1',
...word.toJson(),
},
{'id': '1', ...word.toJson()},
],
'borderWords': const <dynamic>[],
},
);
when(
() => dbClient.find(
sectionsCollection,
{
'position.x': 1,
'position.y': 1,
},
{'position.x': 1, 'position.y': 1},
),
).thenAnswer((_) async => [record]);

when(
() => dbClient.update(
sectionsCollection,
any(that: isA<DbEntityRecord>()),
),
).thenAnswer((_) async {});

final answersRecord = _MockDbEntityRecord();
when(() => answersRecord.id).thenReturn('id1');
when(() => answersRecord.data).thenReturn({'answer': 'flutter'});
when(
() => dbClient.getById(answersCollection, 'id1'),
).thenAnswer((_) async => answersRecord);

repository = CrosswordRepository(dbClient: dbClient);
});

test('answerWord returns true if answer is correct', () async {
test('returns true if answer is correct', () async {
final time = DateTime.now();
final clock = Clock.fixed(time);
await withClock(clock, () async {
final valid =
await repository.answerWord(1, 1, 1, 1, Mascots.dino, 'flutter');
await repository.answerWord(1, 1, '1', Mascots.dino, 'flutter');
expect(valid, isTrue);
final captured = verify(
() => dbClient.update(
Expand Down Expand Up @@ -221,32 +227,76 @@ void main() {
});
});

test('answerWord returns false if answer is incorrect', () async {
test('returns false if answer is incorrect', () async {
final valid =
await repository.answerWord(1, 1, 1, 1, Mascots.dino, 'android');
await repository.answerWord(1, 1, '1', Mascots.dino, 'android');
expect(valid, isFalse);
});

test('answerWord returns false if section does not exist', () async {
test(
'throws $CrosswordRepositoryException if section does not exist',
() async {
when(
() => dbClient.find(
sectionsCollection,
{'position.x': 0, 'position.y': 0},
),
).thenAnswer((_) async => []);

expect(
() => repository.answerWord(0, 0, '1', Mascots.dino, 'flutter'),
throwsA(isA<CrosswordRepositoryException>()),
);
},
);

test(
'throws $CrosswordRepositoryException if word is not in section',
() async {
expect(
() => repository.answerWord(1, 1, 'fake', Mascots.dino, 'flutter'),
throwsA(isA<CrosswordRepositoryException>()),
);
},
);
});

group('updateSolvedWordsCount', () {
late CrosswordRepository repository;

setUp(() {
repository = CrosswordRepository(dbClient: dbClient);

when(
() => dbClient.find(
() => dbClient.update(
sectionsCollection,
{
'position.x': 0,
'position.y': 0,
},
any(),
),
).thenAnswer((_) async => []);

final valid =
await repository.answerWord(0, 0, 1, 1, Mascots.dino, 'flutter');
expect(valid, isFalse);
).thenAnswer((_) async {});
});

test('answerWord returns false if word is not in section', () async {
final valid =
await repository.answerWord(1, 1, -1, -1, Mascots.dino, 'flutter');
expect(valid, isFalse);
test('updates the document in the database', () async {
final record = _MockDbEntityRecord();
when(() => record.id).thenReturn('id');
when(() => record.data).thenReturn({'value': 80});
when(
() => dbClient.find('boardInfo', {'type': 'solved_words_count'}),
).thenAnswer((_) async => [record]);
when(
() => dbClient.update('boardInfo', any()),
).thenAnswer((_) async {});

await repository.updateSolvedWordsCount();

verify(
() => dbClient.update(
'boardInfo',
DbEntityRecord(
id: 'id',
data: {'value': 81},
),
),
).called(1);
});
});
});
Expand Down
Loading

0 comments on commit 5b63c8b

Please sign in to comment.