test: add comprehensive challenge group tests (G1-7)

Add 11 new test cases covering challenge session creation, correct/wrong
answers, idempotent duplicate submission, completion settlement, resource
exhaustion, Plus user bypass, and invalid input validation.

Refactor test helpers to use queue-based mockImplementation pattern for
more reliable db.select mocking across complex async flows.
This commit is contained in:
Wang Zhuoxuan 2026-05-12 10:38:17 +08:00
parent 8801ca1db2
commit 665efa4370
2 changed files with 330 additions and 67 deletions

View File

@ -47,7 +47,7 @@
| G1-4 | 调整红心扣除边界 | [x] | 答错扣 1 颗Plus 用户不被红心阻断;新用户前 3 天最低保留 1 颗 | | G1-4 | 调整红心扣除边界 | [x] | 答错扣 1 颗Plus 用户不被红心阻断;新用户前 3 天最低保留 1 颗 |
| G1-5 | 调整每日高奖励挑战次数 | [x] | 免费用户每日 3 组Plus 8 组或按产品确认无限;次数为 0 后仍可继续学习但高价值奖励降级 | | G1-5 | 调整每日高奖励挑战次数 | [x] | 免费用户每日 3 组Plus 8 组或按产品确认无限;次数为 0 后仍可继续学习但高价值奖励降级 |
| G1-6 | 更新挑战 API DTO | [x] | `/challenges/next` 或新增 session API 能表达组、题、组进度、资源状态 | | G1-6 | 更新挑战 API DTO | [x] | `/challenges/next` 或新增 session API 能表达组、题、组进度、资源状态 |
| G1-7 | 添加挑战组测试 | [ ] | 覆盖创建、答对、答错、重复提交、完成结算、资源不足和 Plus 分支 | | G1-7 | 添加挑战组测试 | [x] | 覆盖创建、答对、答错、重复提交、完成结算、资源不足和 Plus 分支 |
## Phase G2XP、等级、连续学习和知识卡奖励 ## Phase G2XP、等级、连续学习和知识卡奖励

View File

@ -42,6 +42,10 @@ const questions = Array.from({ length: 5 }, (_, index) => ({
updatedAt: null, updatedAt: null,
})); }));
/**
* Creates a mock chain for `select().from().where()` that resolves with `result`.
* Supports `.orderBy()` and `.limit()` after `.where()`.
*/
function selectChain(result: unknown) { function selectChain(result: unknown) {
const whereChain = { const whereChain = {
orderBy: vi.fn().mockResolvedValue(result), orderBy: vi.fn().mockResolvedValue(result),
@ -56,16 +60,41 @@ function selectChain(result: unknown) {
}; };
} }
function selectWithWhere(result: unknown) { /**
* Returns a select mock object that resolves through `.from().where().limit()`.
*/
function selectRows(rows: unknown[]) {
return { return {
from: vi.fn().mockReturnValue({ from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({
limit: vi.fn().mockResolvedValue(result), limit: vi.fn().mockResolvedValue(rows),
then: (resolve: (value: unknown) => unknown) => Promise.resolve(rows).then(resolve),
}), }),
}), }),
}; };
} }
function mockInsert() {
return { values: vi.fn().mockResolvedValue(undefined) } as never;
}
function mockUpdate() {
return { set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) }) } as never;
}
/**
* Sets db.select to consume rows from a queue in order.
* Each call to db.select() returns a mock that resolves to the next queued rows.
*/
function mockSelectQueue(queue: unknown[][]) {
let index = 0;
vi.mocked(db.select).mockImplementation((() => {
const rows = index < queue.length ? queue[index]! : [];
index += 1;
return selectRows(rows);
}) as never);
}
describe('challenge-service', () => { describe('challenge-service', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@ -83,7 +112,6 @@ describe('challenge-service', () => {
}); });
it('applies XP multiplier for degraded rewards', () => { it('applies XP multiplier for degraded rewards', () => {
// multiplier = 0.5 as an example future value; current default is 1
expect(getChallengeCompletionRewards(5, 5, 0.5)).toEqual([ expect(getChallengeCompletionRewards(5, 5, 0.5)).toEqual([
{ type: 'xp', amount: 10, title: '完成挑战 +10 XP' }, { type: 'xp', amount: 10, title: '完成挑战 +10 XP' },
{ type: 'xp', amount: 15, title: '全对奖励 +15 XP' }, { type: 'xp', amount: 15, title: '全对奖励 +15 XP' },
@ -93,47 +121,31 @@ describe('challenge-service', () => {
describe('getHighRewardQuota', () => { describe('getHighRewardQuota', () => {
it('returns full quota when no daily progress exists for free user', async () => { it('returns full quota when no daily progress exists for free user', async () => {
// getHighRewardQuota does: select from userDailyProgress mockSelectQueue([[]]);
vi.mocked(db.select).mockReturnValueOnce(
selectWithWhere([]) as never,
);
const quota = await getHighRewardQuota('user-1', 'free'); const quota = await getHighRewardQuota('user-1', 'free');
expect(quota).toEqual({ max: 3, used: 0, remaining: 3 }); expect(quota).toEqual({ max: 3, used: 0, remaining: 3 });
}); });
it('returns full quota when no daily progress exists for pro user', async () => { it('returns full quota when no daily progress exists for pro user', async () => {
vi.mocked(db.select).mockReturnValueOnce( mockSelectQueue([[]]);
selectWithWhere([]) as never,
);
const quota = await getHighRewardQuota('user-1', 'pro'); const quota = await getHighRewardQuota('user-1', 'pro');
expect(quota).toEqual({ max: 8, used: 0, remaining: 8 }); expect(quota).toEqual({ max: 8, used: 0, remaining: 8 });
}); });
it('returns correct remaining for free user with some used', async () => { it('returns correct remaining for free user with some used', async () => {
vi.mocked(db.select).mockReturnValueOnce( mockSelectQueue([[{ used: 2, restored: 0 }]]);
selectWithWhere([{ used: 2, restored: 0 }]) as never,
);
const quota = await getHighRewardQuota('user-1', 'free'); const quota = await getHighRewardQuota('user-1', 'free');
expect(quota).toEqual({ max: 3, used: 2, remaining: 1 }); expect(quota).toEqual({ max: 3, used: 2, remaining: 1 });
}); });
it('returns zero remaining when quota exhausted', async () => { it('returns zero remaining when quota exhausted', async () => {
vi.mocked(db.select).mockReturnValueOnce( mockSelectQueue([[{ used: 3, restored: 0 }]]);
selectWithWhere([{ used: 3, restored: 0 }]) as never,
);
const quota = await getHighRewardQuota('user-1', 'free'); const quota = await getHighRewardQuota('user-1', 'free');
expect(quota).toEqual({ max: 3, used: 3, remaining: 0 }); expect(quota).toEqual({ max: 3, used: 3, remaining: 0 });
}); });
it('accounts for restored sessions', async () => { it('accounts for restored sessions', async () => {
vi.mocked(db.select).mockReturnValueOnce( mockSelectQueue([[{ used: 3, restored: 1 }]]);
selectWithWhere([{ used: 3, restored: 1 }]) as never,
);
const quota = await getHighRewardQuota('user-1', 'free'); const quota = await getHighRewardQuota('user-1', 'free');
expect(quota).toEqual({ max: 3, used: 2, remaining: 1 }); expect(quota).toEqual({ max: 3, used: 2, remaining: 1 });
}); });
@ -142,21 +154,15 @@ describe('challenge-service', () => {
describe('getNextChallenge', () => { describe('getNextChallenge', () => {
it('creates a challenge session with five questions and hides correct answers', async () => { it('creates a challenge session with five questions and hides correct answers', async () => {
const insertedValues = vi.fn().mockResolvedValue([]); const insertedValues = vi.fn().mockResolvedValue([]);
// getNextChallenge uses selectChain for some queries (with orderBy)
vi.mocked(db.select) vi.mocked(db.select)
// getTrackCategory
.mockReturnValueOnce(selectChain([category]) as never) .mockReturnValueOnce(selectChain([category]) as never)
// getCurrentChapter → chapters
.mockReturnValueOnce(selectChain([chapter]) as never) .mockReturnValueOnce(selectChain([chapter]) as never)
// getCurrentChapter → progress
.mockReturnValueOnce(selectChain([]) as never) .mockReturnValueOnce(selectChain([]) as never)
// getQuestionsForChapter → answered
.mockReturnValueOnce(selectChain([]) as never) .mockReturnValueOnce(selectChain([]) as never)
// getQuestionsForChapter → questions
.mockReturnValueOnce(selectChain(questions) as never) .mockReturnValueOnce(selectChain(questions) as never)
// user tier lookup .mockReturnValueOnce(selectRows([{ tier: 'free' }]) as never)
.mockReturnValueOnce(selectWithWhere([{ tier: 'free' }]) as never) .mockReturnValueOnce(selectRows([]) as never);
// getHighRewardQuota → no daily progress (full quota)
.mockReturnValueOnce(selectWithWhere([]) as never);
vi.mocked(db.insert).mockReturnValue({ values: insertedValues } as never); vi.mocked(db.insert).mockReturnValue({ values: insertedValues } as never);
const result = await getNextChallenge('user-1', 'history'); const result = await getNextChallenge('user-1', 'history');
@ -188,10 +194,8 @@ describe('challenge-service', () => {
.mockReturnValueOnce(selectChain([]) as never) .mockReturnValueOnce(selectChain([]) as never)
.mockReturnValueOnce(selectChain([]) as never) .mockReturnValueOnce(selectChain([]) as never)
.mockReturnValueOnce(selectChain(questions) as never) .mockReturnValueOnce(selectChain(questions) as never)
// user tier .mockReturnValueOnce(selectRows([{ tier: 'free' }]) as never)
.mockReturnValueOnce(selectWithWhere([{ tier: 'free' }]) as never) .mockReturnValueOnce(selectRows([{ used: 3, restored: 0 }]) as never);
// getHighRewardQuota → used=3, restored=0 (exhausted)
.mockReturnValueOnce(selectWithWhere([{ used: 3, restored: 0 }]) as never);
vi.mocked(db.insert).mockReturnValue({ values: insertedValues } as never); vi.mocked(db.insert).mockReturnValue({ values: insertedValues } as never);
const result = await getNextChallenge('user-1', 'history'); const result = await getNextChallenge('user-1', 'history');
@ -210,51 +214,74 @@ describe('challenge-service', () => {
.mockReturnValueOnce(selectChain([]) as never) .mockReturnValueOnce(selectChain([]) as never)
.mockReturnValueOnce(selectChain([]) as never) .mockReturnValueOnce(selectChain([]) as never)
.mockReturnValueOnce(selectChain(questions) as never) .mockReturnValueOnce(selectChain(questions) as never)
.mockReturnValueOnce(selectWithWhere([{ tier: 'pro' }]) as never) .mockReturnValueOnce(selectRows([{ tier: 'pro' }]) as never)
.mockReturnValueOnce(selectWithWhere([{ used: 5, restored: 0 }]) as never); .mockReturnValueOnce(selectRows([{ used: 5, restored: 0 }]) as never);
vi.mocked(db.insert).mockReturnValue({ values: insertedValues } as never); vi.mocked(db.insert).mockReturnValue({ values: insertedValues } as never);
const result = await getNextChallenge('user-1', 'history'); const result = await getNextChallenge('user-1', 'history');
// Pro user with 5 used out of 8 → still eligible
expect(result?.highRewardEligible).toBe(true); expect(result?.highRewardEligible).toBe(true);
}); });
}); });
describe('submitChallengeAnswer', () => { describe('submitChallengeAnswer', () => {
function makeSession(overrides: Record<string, unknown> = {}) {
return {
id: 'challenge-1',
userId: 'user-1',
status: 'in_progress',
questionIds: ['q-1', 'q-2', 'q-3', 'q-4', 'q-5'],
totalQuestions: 5,
answeredCount: 0,
correctCount: 0,
highRewardEligible: 1,
...overrides,
};
}
const testQuestion = {
id: 'q-1',
stem: { text: '测试题' },
contentType: 'text',
correctAnswer: '正确答案',
distractors: ['干扰项1', '干扰项2'],
categoryId: 'history',
difficulty: 1,
dynamicDifficulty: null,
source: 'system',
creatorId: null,
status: 'published',
stats: { timesAnswered: 0, correctRate: 0, avgTimeMs: 0 },
createdAt: null,
updatedAt: null,
};
const knowledgeCardRow = {
id: 'card-1',
questionId: 'q-1',
summary: '知识点摘要',
deepDive: '深入解析',
};
const freeUserRow = {
id: 'user-1', tier: 'free', xpTotal: 100, activeTrackId: null,
dailyAttemptsLeft: 5, dailyAttemptsDate: new Date().toISOString(),
checkInDays: 1, lastCheckInDate: null, streakProtectedUntil: null,
};
it('returns the stored result for duplicate question submissions without side effects', async () => { it('returns the stored result for duplicate question submissions without side effects', async () => {
const resultSnapshot = { const resultSnapshot = {
answerState: 'correct', answerState: 'correct',
correctOptionId: 'a', correctOptionId: 'a',
xpDelta: 10, xpDelta: 10,
progress: { progress: { hearts: 5, dailyAttemptsLeft: 3, xp: 120, streakDays: 2 },
hearts: 5, knowledgeCard: { id: 'card-1', title: '知识点', summary: '知识点', fact: '解析' },
dailyAttemptsLeft: 3,
xp: 120,
streakDays: 2,
},
knowledgeCard: {
id: 'card-1',
title: '知识点',
summary: '知识点',
fact: '解析',
},
rewards: [{ type: 'xp', amount: 10, title: '+10 XP' }], rewards: [{ type: 'xp', amount: 10, title: '+10 XP' }],
}; };
vi.mocked(db.select) mockSelectQueue([
.mockReturnValueOnce(selectChain([{ [{ id: 'challenge-1', userId: 'user-1', status: 'pending', questionIds: ['question-1'] }],
id: 'challenge-1', [{ id: 'answer-1', sessionId: 'challenge-1', questionId: 'question-1', submitRequestId: 'submit-1', resultSnapshot }],
userId: 'user-1', ]);
status: 'pending',
questionIds: ['question-1'],
}]) as never)
.mockReturnValueOnce(selectChain([{
id: 'answer-1',
sessionId: 'challenge-1',
questionId: 'question-1',
submitRequestId: 'submit-1',
resultSnapshot,
}]) as never);
const result = await submitChallengeAnswer('user-1', 'challenge-1', 'question-1', 'a', 1200, 0, 'submit-1'); const result = await submitChallengeAnswer('user-1', 'challenge-1', 'question-1', 'a', 1200, 0, 'submit-1');
@ -262,5 +289,241 @@ describe('challenge-service', () => {
expect(db.insert).not.toHaveBeenCalled(); expect(db.insert).not.toHaveBeenCalled();
expect(db.update).not.toHaveBeenCalled(); expect(db.update).not.toHaveBeenCalled();
}); });
it('throws NotFoundError when session does not exist', async () => {
mockSelectQueue([[]]);
await expect(
submitChallengeAnswer('user-1', 'nonexistent', 'q-1', 'a', 1000),
).rejects.toThrow('Challenge');
});
it('throws ValidationError when session is already completed', async () => {
mockSelectQueue([[makeSession({ status: 'completed' })]]);
await expect(
submitChallengeAnswer('user-1', 'challenge-1', 'q-1', 'a', 1000),
).rejects.toThrow('not accepting answers');
});
it('throws ValidationError when question is not in session', async () => {
mockSelectQueue([[makeSession()], []]);
await expect(
submitChallengeAnswer('user-1', 'challenge-1', 'unknown-question', 'a', 1000),
).rejects.toThrow('does not belong');
});
it('awards XP for a correct answer', async () => {
mockSelectQueue([
[makeSession()], // session
[], // no existing answer
[testQuestion], // question
[{ id: 'up-1' }], // getCorrectAnswersToday
[freeUserRow], // getResourceUser (getProgressSummary)
[{ tier: 'free', heartsRemaining: 5, heartsLastRestore: null }], // getHearts
[{ checkInDays: 2, lastCheckInDate: new Date().toISOString() }], // calculateStreak
[], // getSubscriptionStatus
[freeUserRow], // getDailyAttempts
[{ used: 0, restored: 0 }], // getHighRewardQuota
[knowledgeCardRow], // getKnowledgeCard
]);
vi.mocked(db.insert).mockReturnValue(mockInsert());
vi.mocked(db.update).mockReturnValue(mockUpdate());
const result = await submitChallengeAnswer('user-1', 'challenge-1', 'q-1', 'c', 1200, 0);
expect(result.answerState).toBe('correct');
expect(result.correctOptionId).toBe('c');
expect(result.xpDelta).toBe(10);
expect(result.rewards).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: 'xp', amount: 10 }),
]),
);
expect(result.knowledgeCard.id).toBe('card-1');
});
it('deducts a heart for a wrong answer', async () => {
const userAfter = { ...freeUserRow, dailyAttemptsLeft: 4 };
mockSelectQueue([
[makeSession()], // session
[], // no existing answer
[testQuestion], // question
[{ tier: 'free', heartsRemaining: 3 }], // deductHeart: user
[{ createdAt: new Date(Date.now() - 10 * 86_400_000).toISOString() }], // isNewUserProtected
[freeUserRow], // deductDailyAttempt → getResourceUser
[userAfter], // getResourceUser (getProgressSummary)
[{ tier: 'free', heartsRemaining: 2, heartsLastRestore: null }], // getHearts
[{ checkInDays: 0, lastCheckInDate: null }], // calculateStreak
[], // getSubscriptionStatus
[userAfter], // getDailyAttempts
[{ used: 0, restored: 0 }], // getHighRewardQuota
[knowledgeCardRow], // getKnowledgeCard
]);
vi.mocked(db.insert).mockReturnValue(mockInsert());
vi.mocked(db.update).mockReturnValue(mockUpdate());
const result = await submitChallengeAnswer('user-1', 'challenge-1', 'q-1', 'b', 2000, 0);
expect(result.answerState).toBe('wrong');
expect(result.xpDelta).toBe(0);
expect(db.update).toHaveBeenCalled();
});
it('throws ValidationError when hearts are exhausted on wrong answer', async () => {
mockSelectQueue([
[makeSession()], // session
[], // no existing answer
[testQuestion], // question
[{ tier: 'free', heartsRemaining: 0 }], // deductHeart: user
[{ createdAt: new Date(Date.now() - 10 * 86_400_000).toISOString() }], // isNewUserProtected
]);
vi.mocked(db.insert).mockReturnValue(mockInsert());
vi.mocked(db.update).mockReturnValue(mockUpdate());
await expect(
submitChallengeAnswer('user-1', 'challenge-1', 'q-1', 'b', 2000, 0),
).rejects.toThrow('红心已用完');
});
it('does not block Plus users when hearts are depleted', async () => {
const proUserRow = { ...freeUserRow, tier: 'pro', xpTotal: 200, dailyAttemptsLeft: 10 };
const proUserAfter = { ...proUserRow, dailyAttemptsLeft: 9 };
mockSelectQueue([
[makeSession()], // session
[], // no existing answer
[testQuestion], // question
[{ tier: 'pro', heartsRemaining: 99 }], // deductHeart: pro user
[proUserRow], // deductDailyAttempt → getResourceUser
[proUserAfter], // getResourceUser (getProgressSummary)
[{ tier: 'pro', heartsRemaining: 99, heartsLastRestore: null }], // getHearts
[{ checkInDays: 0, lastCheckInDate: null }], // calculateStreak
[], // getSubscriptionStatus
[proUserAfter], // getDailyAttempts
[{ used: 0, restored: 0 }], // getHighRewardQuota
[knowledgeCardRow], // getKnowledgeCard
]);
vi.mocked(db.insert).mockReturnValue(mockInsert());
vi.mocked(db.update).mockReturnValue(mockUpdate());
const result = await submitChallengeAnswer('user-1', 'challenge-1', 'q-1', 'b', 2000, 0);
expect(result.answerState).toBe('wrong');
expect(result.progress.hearts).toBe(99);
});
it('triggers completion settlement on the last question', async () => {
const userAfterXp = { ...freeUserRow, xpTotal: 150 };
mockSelectQueue([
[makeSession({ answeredCount: 4, correctCount: 4 })], // session
[], // no existing answer
[testQuestion], // question (but we submit q-5)
[{ id: 'up-1' }], // getCorrectAnswersToday
// settleCompletedChallenge → getProgressSummary (before)
[freeUserRow], // getResourceUser
[{ tier: 'free', heartsRemaining: 5, heartsLastRestore: null }], // getHearts
[{ checkInDays: 2, lastCheckInDate: new Date().toISOString() }], // calculateStreak
[], // getSubscriptionStatus
[freeUserRow], // getDailyAttempts
[{ used: 0, restored: 0 }], // getHighRewardQuota
// updateChapterProgress
[{ id: 'chapter-1', passThreshold: 3 }],
[], // no existing chapter progress
[], // no existing daily progress
// getProgressSummary (final)
[userAfterXp],
[{ tier: 'free', heartsRemaining: 5, heartsLastRestore: null }],
[{ checkInDays: 2, lastCheckInDate: new Date().toISOString() }],
[],
[userAfterXp],
[{ used: 1, restored: 0 }],
[knowledgeCardRow],
]);
vi.mocked(db.insert).mockReturnValue(mockInsert());
vi.mocked(db.update).mockReturnValue(mockUpdate());
const result = await submitChallengeAnswer('user-1', 'challenge-1', 'q-5', 'c', 1500, 0);
expect(result.answerState).toBe('correct');
// 10 XP (correct answer) + 20 (complete) + 30 (perfect) = 60
expect(result.xpDelta).toBe(60);
expect(result.rewards).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: 'xp', amount: 10, title: '+10 XP' }),
expect.objectContaining({ type: 'xp', amount: 20, title: '完成挑战 +20 XP' }),
expect.objectContaining({ type: 'xp', amount: 30, title: '全对奖励 +30 XP' }),
]),
);
});
it('gives completion XP but no perfect bonus when not all correct', async () => {
const userBefore = { ...freeUserRow, dailyAttemptsLeft: 4 };
const userFinal = { ...freeUserRow, xpTotal: 120, dailyAttemptsLeft: 4 };
mockSelectQueue([
[makeSession({ answeredCount: 4, correctCount: 3 })], // session
[], // no existing answer
[testQuestion], // question
// wrong answer path
[{ tier: 'free', heartsRemaining: 3 }], // deductHeart
[{ createdAt: new Date(Date.now() - 10 * 86_400_000).toISOString() }], // isNewUserProtected
[freeUserRow], // deductDailyAttempt → getResourceUser
// settleCompletedChallenge → getProgressSummary (before)
[userBefore],
[{ tier: 'free', heartsRemaining: 2, heartsLastRestore: null }],
[{ checkInDays: 0, lastCheckInDate: null }],
[],
[userBefore],
[{ used: 0, restored: 0 }],
// updateChapterProgress
[{ id: 'chapter-1', passThreshold: 3 }],
[],
[], // updateDailyProgress
// getProgressSummary (final)
[userFinal],
[{ tier: 'free', heartsRemaining: 2, heartsLastRestore: null }],
[{ checkInDays: 0, lastCheckInDate: null }],
[],
[userFinal],
[{ used: 1, restored: 0 }],
[knowledgeCardRow],
]);
vi.mocked(db.insert).mockReturnValue(mockInsert());
vi.mocked(db.update).mockReturnValue(mockUpdate());
const result = await submitChallengeAnswer('user-1', 'challenge-1', 'q-5', 'b', 2000, 0);
expect(result.answerState).toBe('wrong');
expect(result.xpDelta).toBe(20);
const rewardTitles = result.rewards.map((r) => r.title);
expect(rewardTitles).toContain('完成挑战 +20 XP');
expect(rewardTitles).not.toContain(expect.stringContaining('全对'));
});
it('throws ValidationError for invalid selectedOptionId', async () => {
mockSelectQueue([
[makeSession()], // session
[], // no existing answer
[testQuestion], // question
]);
vi.mocked(db.insert).mockReturnValue(mockInsert());
vi.mocked(db.update).mockReturnValue(mockUpdate());
await expect(
submitChallengeAnswer('user-1', 'challenge-1', 'q-1', 'z', 1000, 0),
).rejects.toThrow('Invalid selectedOptionId');
});
it('throws NotFoundError when question does not exist in DB', async () => {
mockSelectQueue([
[makeSession()], // session
[], // no existing answer
[], // question not found
]);
await expect(
submitChallengeAnswer('user-1', 'challenge-1', 'q-1', 'a', 1000, 0),
).rejects.toThrow('Question');
});
}); });
}); });