Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update answer word endpoint #311

Merged
merged 7 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
jsgalarraga marked this conversation as resolved.
Show resolved Hide resolved
int wordX,
int wordY,
String wordId,
Mascots mascot,
AyadLaouissi marked this conversation as resolved.
Show resolved Hide resolved
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
Loading