353 lines
9.9 KiB
Dart
353 lines
9.9 KiB
Dart
@Tags(<String>['integration', 'postgres'])
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:cyberhybridhub_server/pipeline/question_pipeline.dart';
|
|
import 'package:cyberhybridhub_server/question_dispatcher.dart';
|
|
import 'package:postgres/postgres.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../helpers/test_db.dart';
|
|
|
|
void main() {
|
|
TestDb? testDb;
|
|
|
|
setUpAll(() async {
|
|
testDb = await TestDb.open();
|
|
});
|
|
|
|
tearDown(() async {
|
|
if (testDb != null) {
|
|
await testDb!.truncateTradingTables();
|
|
}
|
|
});
|
|
|
|
tearDownAll(() async {
|
|
await testDb?.close();
|
|
});
|
|
|
|
Future<QuestionPipeline> _pipeline() async {
|
|
return QuestionPipeline(
|
|
questionsDb: testDb!.questionsDb,
|
|
questionService: testDb!.questionService(),
|
|
questionSourcePreferencesDb: testDb!.questionSourcePreferencesDb,
|
|
testMode: true,
|
|
);
|
|
}
|
|
|
|
test('maintainSpellingForUser creates spelling pipeline question', () async {
|
|
if (testDb == null) {
|
|
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
|
|
return;
|
|
}
|
|
|
|
const String uid = 'spelling-maintain-uid';
|
|
await testDb!.seedUser(uid);
|
|
|
|
final QuestionPipeline pipeline = await _pipeline();
|
|
await pipeline.maintainSpellingForUser(uid);
|
|
|
|
final Result rows = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT pipeline_key, pipeline_step, source_tag
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
expect(rows, hasLength(1));
|
|
expect(rows.first[0], PipelineKeys.spelling);
|
|
expect(rows.first[1], PipelineSteps.spellingWordQuiz);
|
|
expect(rows.first[2], 'spelling:word_quiz');
|
|
|
|
final Result meta = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT metadata->>'spelling_item_id'
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
expect(meta.first[0], isNull);
|
|
});
|
|
|
|
test('prepareSpellingQuestion selects word and creates links at tile load',
|
|
() async {
|
|
if (testDb == null) {
|
|
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
|
|
return;
|
|
}
|
|
|
|
const String uid = 'spelling-prepare-uid';
|
|
await testDb!.seedUser(uid);
|
|
|
|
final QuestionPipeline pipeline = await _pipeline();
|
|
await pipeline.maintainSpellingForUser(uid);
|
|
|
|
final Result reserved = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT id, metadata
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
final String questionId = reserved.first[0]!.toString();
|
|
|
|
final Map<String, dynamic>? prepared = await pipeline.prepareSpellingQuestion(
|
|
firebaseUid: uid,
|
|
questionId: questionId,
|
|
);
|
|
expect(prepared, isNotNull);
|
|
expect(prepared!['spokenWord'], isNotEmpty);
|
|
expect(prepared['audioTitle'], questionId);
|
|
expect(prepared['canonicalWord'], isNotEmpty);
|
|
|
|
final Result link = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT spelling_item_id
|
|
FROM spelling_question_links
|
|
WHERE question_id = @qid::uuid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'qid': questionId},
|
|
);
|
|
expect(link, hasLength(1));
|
|
|
|
final Result progress = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT familiarity_score, attempt_count
|
|
FROM user_spelling_progress
|
|
WHERE firebase_uid = @uid
|
|
AND spelling_item_id = @item_id::uuid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{
|
|
'uid': uid,
|
|
'item_id': link.first[0]!.toString(),
|
|
},
|
|
);
|
|
expect(progress, hasLength(1));
|
|
expect(progress.first[0], 0);
|
|
expect(progress.first[1], 0);
|
|
});
|
|
|
|
test('maintainSpellingForUser skips when spelling source disabled', () async {
|
|
if (testDb == null) {
|
|
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
|
|
return;
|
|
}
|
|
|
|
const String uid = 'spelling-disabled-uid';
|
|
await testDb!.seedUser(uid);
|
|
await testDb!.questionSourcePreferencesDb.save(
|
|
uid,
|
|
<Map<String, dynamic>>[
|
|
<String, dynamic>{'id': 'market_history', 'enabled': true, 'ratio': 50},
|
|
<String, dynamic>{'id': 'spelling', 'enabled': false, 'ratio': 50},
|
|
],
|
|
);
|
|
|
|
final QuestionPipeline pipeline = await _pipeline();
|
|
await pipeline.maintainSpellingForUser(uid);
|
|
|
|
final Result rows = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT COUNT(*)::int
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
expect(rows.first[0], 0);
|
|
});
|
|
|
|
test('onAnswerSubmitted branches to follow-up then another word', () async {
|
|
if (testDb == null) {
|
|
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
|
|
return;
|
|
}
|
|
|
|
const String uid = 'spelling-answer-uid';
|
|
await testDb!.seedUser(uid);
|
|
|
|
final QuestionPipeline pipeline = await _pipeline();
|
|
await pipeline.maintainSpellingForUser(uid);
|
|
|
|
final Result reserved = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT id
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
ORDER BY created_at
|
|
LIMIT 1
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
final String reservedId = reserved.first[0]!.toString();
|
|
await pipeline.prepareSpellingQuestion(
|
|
firebaseUid: uid,
|
|
questionId: reservedId,
|
|
);
|
|
|
|
final Result first = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT id, metadata
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
ORDER BY created_at
|
|
LIMIT 1
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
final String questionId = first.first[0]!.toString();
|
|
final Object? metadataRaw = first.first[1];
|
|
final Map<String, dynamic> metadata = metadataRaw is Map<String, dynamic>
|
|
? metadataRaw
|
|
: Map<String, dynamic>.from(
|
|
jsonDecode(metadataRaw.toString()) as Map<String, dynamic>,
|
|
);
|
|
final String canonicalWord = metadata['canonical_word']! as String;
|
|
|
|
final Map<String, dynamic>? answered = await testDb!.questionsDb.submitAnswer(
|
|
questionId: questionId,
|
|
assignedUserId: uid,
|
|
userResponseText: canonicalWord,
|
|
);
|
|
expect(answered, isNotNull);
|
|
|
|
await pipeline.onAnswerSubmitted(
|
|
firebaseUid: uid,
|
|
answeredQuestion: answered!,
|
|
userResponse: answered['userResponse'] as num? ?? 0,
|
|
);
|
|
|
|
final Result rows = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT pipeline_step, source_tag
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
ORDER BY created_at
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
expect(rows.length, greaterThanOrEqualTo(2));
|
|
expect(rows.last[0], PipelineSteps.spellingFollowUp);
|
|
expect(rows.last[1], 'spelling:follow_up');
|
|
});
|
|
|
|
test('resetSpellingProgressForUser clears links and progress then re-seeds',
|
|
() async {
|
|
if (testDb == null) {
|
|
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
|
|
return;
|
|
}
|
|
|
|
const String uid = 'spelling-reset-uid';
|
|
await testDb!.seedUser(uid);
|
|
|
|
final QuestionDispatcher dispatcher = testDb!.questionDispatcher();
|
|
final QuestionPipeline pipeline = testDb!.questionPipeline(testMode: true);
|
|
pipeline.attachQuestionDispatcher(dispatcher);
|
|
await pipeline.maintainSpellingForUser(uid);
|
|
|
|
final Result reserved = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT id
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
LIMIT 1
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
final String questionId = reserved.first[0]!.toString();
|
|
await pipeline.prepareSpellingQuestion(
|
|
firebaseUid: uid,
|
|
questionId: questionId,
|
|
);
|
|
|
|
final Result progressBefore = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT COUNT(*)::int
|
|
FROM user_spelling_progress
|
|
WHERE firebase_uid = @uid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
final Result linksBefore = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT COUNT(*)::int
|
|
FROM spelling_question_links
|
|
WHERE firebase_uid = @uid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
expect(progressBefore.first[0], greaterThan(0));
|
|
expect(linksBefore.first[0], greaterThan(0));
|
|
|
|
final Map<String, dynamic> result =
|
|
await pipeline.resetSpellingProgressForUser(uid);
|
|
expect(result['unansweredCount'], greaterThanOrEqualTo(1));
|
|
|
|
final Result progressAfter = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT COUNT(*)::int
|
|
FROM user_spelling_progress
|
|
WHERE firebase_uid = @uid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
final Result linksAfter = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT COUNT(*)::int
|
|
FROM spelling_question_links
|
|
WHERE firebase_uid = @uid
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
expect(progressAfter.first[0], 0);
|
|
expect(linksAfter.first[0], 0);
|
|
|
|
final Result spellingQuestions = await testDb!.connection.execute(
|
|
Sql.named(
|
|
'''
|
|
SELECT COUNT(*)::int
|
|
FROM questions
|
|
WHERE assigned_user_id = @uid
|
|
AND pipeline_key = 'spelling'
|
|
AND user_response IS NULL
|
|
''',
|
|
),
|
|
parameters: <String, dynamic>{'uid': uid},
|
|
);
|
|
expect(spellingQuestions.first[0], 1);
|
|
});
|
|
}
|