Skip to content

Commit d4f04f7

Browse files
committed
improve: whitespace and clarity in exercise-logic tests, strengthen type safety and schema validation
1 parent e362234 commit d4f04f7

File tree

1 file changed

+74
-73
lines changed

1 file changed

+74
-73
lines changed

app/actions/exercise-logic.test.ts

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import * as logic from './exercise-logic';
33
import { generateAndValidateExercise } from '@/lib/ai/exercise-generator';
44
import { saveExerciseToCache, getValidatedExerciseFromCache } from '@/lib/exercise-cache';
5+
import { ExerciseRequestParamsSchema, GenerateExerciseResultSchema } from '@/lib/domain/schemas';
56
import type { ExerciseGenerationParams } from '@/lib/domain/ai';
67
import type {
78
ExerciseContent,
@@ -21,7 +22,7 @@ const {
2122
getOrGenerateExercise,
2223
} = logic;
2324

24-
const mockParams: ExerciseGenerationParams = {
25+
const mockParams = {
2526
topic: 'Test Topic',
2627
passageLanguage: 'en',
2728
questionLanguage: 'es',
@@ -30,28 +31,28 @@ const mockParams: ExerciseGenerationParams = {
3031
level: 'B1',
3132
grammarGuidance: 'Past tense',
3233
vocabularyGuidance: 'Travel words',
33-
};
34+
} as const satisfies ExerciseGenerationParams;
3435

35-
const mockRequestParams: ExerciseRequestParams = {
36+
const mockRequestParams = {
3637
passageLanguage: 'en',
3738
questionLanguage: 'es',
3839
cefrLevel: 'B1',
39-
};
40+
} as const satisfies ExerciseRequestParams;
4041

4142
const mockUserId = 123;
4243
const mockLanguage = 'en';
4344

44-
const mockExerciseContent: ExerciseContent = {
45+
const mockExerciseContent = {
4546
paragraph: 'p',
4647
question: 'q',
4748
options: { A: 'A', B: 'B', C: 'C', D: 'D' },
4849
correctAnswer: 'A',
4950
allExplanations: { A: 'EA', B: 'EB', C: 'EC', D: 'ED' },
5051
relevantText: 'rt',
5152
topic: 'Test Topic',
52-
};
53+
} as const satisfies ExerciseContent;
5354

54-
const mockCacheResult: GenerateExerciseResult = {
55+
const mockCacheResult = {
5556
quizData: {
5657
paragraph: 'cached_p',
5758
question: 'cached_q',
@@ -62,7 +63,7 @@ const mockCacheResult: GenerateExerciseResult = {
6263
quizId: 999,
6364
cached: true,
6465
error: null,
65-
};
66+
} as const satisfies GenerateExerciseResult;
6667

6768
describe('Exercise Logic Functions', () => {
6869
beforeEach(() => {
@@ -91,25 +92,18 @@ describe('Exercise Logic Functions', () => {
9192
mockUserId
9293
);
9394
});
94-
it('returns failure if generateAndValidateExercise throws an error', async () => {
95-
const error = new Error('AI failed');
96-
vi.mocked(generateAndValidateExercise).mockRejectedValue(error);
95+
it.each([
96+
[new Error('AI failed'), 'Error during AI generation/processing: AI failed'],
97+
[new Error('Generic failure'), 'Error during AI generation/processing: Generic failure'],
98+
[123, 'Error during AI generation/processing: 123'],
99+
[null, 'Error during AI generation/processing: null'],
100+
])('returns failure if generateAndValidateExercise throws %p', async (thrown, expectedMsg) => {
101+
vi.mocked(generateAndValidateExercise).mockRejectedValue(thrown);
97102
const result = await tryGenerateAndCacheExercise(mockParams, mockLanguage, mockUserId);
98103
expect(result.success).toBe(false);
99104
if (!result.success) {
100-
expect(result.error.error).toContain('Error during AI generation/processing: AI failed');
101-
}
102-
expect(saveExerciseToCache).not.toHaveBeenCalled();
103-
});
104-
it('returns failure if generateAndValidateExercise throws a non-standard error', async () => {
105-
const genericError = new Error('Generic failure');
106-
vi.mocked(generateAndValidateExercise).mockRejectedValue(genericError);
107-
const result = await tryGenerateAndCacheExercise(mockParams, mockLanguage, mockUserId);
108-
expect(result.success).toBe(false);
109-
if (!result.success) {
110-
expect(result.error.error).toContain(
111-
'Error during AI generation/processing: Generic failure'
112-
);
105+
expect(result.error.error).toContain(expectedMsg);
106+
expect(typeof result.error.error).toBe('string');
113107
}
114108
expect(saveExerciseToCache).not.toHaveBeenCalled();
115109
});
@@ -125,17 +119,27 @@ describe('Exercise Logic Functions', () => {
125119
}
126120
});
127121
it('returns failure if saveExerciseToCache throws an error', async () => {
128-
const cacheError = new Error('Cache save failed');
129122
vi.mocked(generateAndValidateExercise).mockResolvedValue(mockExerciseContent);
130123
vi.mocked(saveExerciseToCache).mockImplementation(() => {
131-
throw cacheError;
124+
throw new Error('Cache save failed');
132125
});
133126
const result = await tryGenerateAndCacheExercise(mockParams, mockLanguage, mockUserId);
134127
expect(result.success).toBe(false);
135128
if (!result.success) {
136129
expect(result.error.error).toBe('Error during AI generation/processing: Cache save failed');
137130
}
138131
});
132+
it('returns failure if saveExerciseToCache throws a non-Error value', async () => {
133+
vi.mocked(generateAndValidateExercise).mockResolvedValue(mockExerciseContent);
134+
vi.mocked(saveExerciseToCache).mockImplementation(() => {
135+
throw new Error('123');
136+
});
137+
const result = await tryGenerateAndCacheExercise(mockParams, mockLanguage, mockUserId);
138+
expect(result.success).toBe(false);
139+
if (!result.success) {
140+
expect(result.error.error).toBe('Error during AI generation/processing: 123');
141+
}
142+
});
139143
});
140144

141145
describe('tryGetCachedExercise', () => {
@@ -152,17 +156,12 @@ describe('Exercise Logic Functions', () => {
152156
mockRequestParams.cefrLevel,
153157
mockUserId
154158
);
159+
expect(GenerateExerciseResultSchema.safeParse(result).success).toBe(true);
155160
});
156161
it('returns null if no cached exercise is found', async () => {
157162
vi.mocked(getValidatedExerciseFromCache).mockReturnValue(undefined);
158163
const result = await tryGetCachedExercise(mockRequestParams, mockUserId);
159164
expect(result).toBeNull();
160-
expect(getValidatedExerciseFromCache).toHaveBeenCalledWith(
161-
mockRequestParams.passageLanguage,
162-
mockRequestParams.questionLanguage,
163-
mockRequestParams.cefrLevel,
164-
mockUserId
165-
);
166165
});
167166
});
168167

@@ -176,6 +175,7 @@ describe('Exercise Logic Functions', () => {
176175
error: errorMsg,
177176
cached: false,
178177
});
178+
expect(GenerateExerciseResultSchema.safeParse(response).success).toBe(true);
179179
});
180180
it('includes details if provided', () => {
181181
const errorMsg = 'Test error';
@@ -187,6 +187,7 @@ describe('Exercise Logic Functions', () => {
187187
error: `${errorMsg}: ${JSON.stringify(details)}`,
188188
cached: false,
189189
});
190+
expect(GenerateExerciseResultSchema.safeParse(response).success).toBe(true);
190191
});
191192
});
192193

@@ -199,17 +200,22 @@ describe('Exercise Logic Functions', () => {
199200
expect(result.data.passageLanguage).toBe(validParams.passageLanguage);
200201
expect(result.data.questionLanguage).toBe(validParams.questionLanguage);
201202
expect(result.data.cefrLevel).toBe(validParams.cefrLevel);
203+
expect(ExerciseRequestParamsSchema.safeParse(result.data).success).toBe(true);
202204
}
203205
});
204206
it('invalidates incorrect params', () => {
205207
const invalidParams = { passageLanguage: 'en', questionLanguage: 'fr', cefrLevel: 'Z9' };
206208
const result = validateRequestParams(invalidParams);
207209
expect(result.success).toBe(false);
208210
});
211+
it('invalidates missing params', () => {
212+
const result = validateRequestParams({});
213+
expect(result.success).toBe(false);
214+
});
209215
});
210216

211217
describe('getOrGenerateExercise', () => {
212-
const validGenParams: ExerciseGenerationParams = {
218+
const validGenParams = {
213219
passageLanguage: 'en',
214220
questionLanguage: 'fr',
215221
level: 'A2',
@@ -218,10 +224,10 @@ describe('Exercise Logic Functions', () => {
218224
questionLangName: 'French',
219225
grammarGuidance: 'Mock Grammar',
220226
vocabularyGuidance: 'Mock Vocab',
221-
};
227+
} as const satisfies ExerciseGenerationParams;
222228
const mockUserId = 1;
223229
const generatedSuccessData = { content: mockExerciseContent, id: 555 };
224-
const cachedResult: GenerateExerciseResult = {
230+
const cachedResult = {
225231
quizData: {
226232
paragraph: 'Cached Para',
227233
question: 'Cached Q',
@@ -232,35 +238,46 @@ describe('Exercise Logic Functions', () => {
232238
quizId: 999,
233239
cached: true,
234240
error: null,
235-
};
241+
} as const satisfies GenerateExerciseResult;
236242
beforeEach(() => {
237243
vi.resetAllMocks();
238244
});
239-
it('returns generated exercise if cache is low and generation succeeds', async () => {
240-
vi.mocked(generateAndValidateExercise).mockResolvedValue(mockExerciseContent);
241-
vi.mocked(saveExerciseToCache).mockReturnValue(555);
242-
vi.mocked(getValidatedExerciseFromCache).mockReturnValue(undefined);
243-
const result = await getOrGenerateExercise(validGenParams, mockUserId, 10);
244-
expect(result.quizId).toBe(generatedSuccessData.id);
245-
expect(result.cached).toBe(false);
246-
expect(result.error).toBeNull();
247-
expect(result.quizData.paragraph).toBe(mockExerciseContent.paragraph);
248-
});
249-
it('returns cached exercise if cache is low, generation fails, but cache fallback succeeds', async () => {
250-
vi.mocked(generateAndValidateExercise).mockRejectedValue(new Error('Generation Failed'));
251-
vi.mocked(getValidatedExerciseFromCache).mockReturnValue({
252-
quizData: cachedResult.quizData,
253-
quizId: cachedResult.quizId,
254-
});
255-
const result = await getOrGenerateExercise(validGenParams, mockUserId, 10);
256-
expect(result).toEqual(cachedResult);
257-
});
245+
it.each([
246+
[10, false],
247+
[99, false],
248+
[100, true],
249+
[200, true],
250+
])(
251+
'returns correct result for cachedCount=%i (preferGenerate=%s)',
252+
async (cachedCount, preferCache) => {
253+
if (preferCache) {
254+
vi.mocked(getValidatedExerciseFromCache).mockReturnValue({
255+
quizData: cachedResult.quizData,
256+
quizId: cachedResult.quizId,
257+
});
258+
const result = await getOrGenerateExercise(validGenParams, mockUserId, cachedCount);
259+
expect(result).toEqual(cachedResult);
260+
expect(GenerateExerciseResultSchema.safeParse(result).success).toBe(true);
261+
} else {
262+
vi.mocked(generateAndValidateExercise).mockResolvedValue(mockExerciseContent);
263+
vi.mocked(saveExerciseToCache).mockReturnValue(555);
264+
vi.mocked(getValidatedExerciseFromCache).mockReturnValue(undefined);
265+
const result = await getOrGenerateExercise(validGenParams, mockUserId, cachedCount);
266+
expect(result.quizId).toBe(generatedSuccessData.id);
267+
expect(result.cached).toBe(false);
268+
expect(result.error).toBeNull();
269+
expect(result.quizData.paragraph).toBe(mockExerciseContent.paragraph);
270+
expect(GenerateExerciseResultSchema.safeParse(result).success).toBe(true);
271+
}
272+
}
273+
);
258274
it('returns generation error if cache is low, generation and cache fallback both fail', async () => {
259275
vi.mocked(generateAndValidateExercise).mockRejectedValue(new Error('Generation Failed'));
260276
vi.mocked(getValidatedExerciseFromCache).mockReturnValue(undefined);
261277
const result = await getOrGenerateExercise(validGenParams, mockUserId, 10);
262278
expect(result.error).toContain('Generation Failed');
263279
expect(result.quizId).toBe(-1);
280+
expect(GenerateExerciseResultSchema.safeParse(result).success).toBe(true);
264281
});
265282
it('returns generation error if cache is low and generation fails (terminal cache error)', async () => {
266283
vi.mocked(generateAndValidateExercise).mockResolvedValue(mockExerciseContent);
@@ -271,31 +288,15 @@ describe('Exercise Logic Functions', () => {
271288
'Exercise generated but failed to save to cache (undefined ID).'
272289
);
273290
expect(result.quizId).toBe(-1);
274-
});
275-
it('returns cached exercise if cache is high and cache lookup succeeds', async () => {
276-
vi.mocked(getValidatedExerciseFromCache).mockReturnValue({
277-
quizData: cachedResult.quizData,
278-
quizId: cachedResult.quizId,
279-
});
280-
const result = await getOrGenerateExercise(validGenParams, mockUserId, 200);
281-
expect(result).toEqual(cachedResult);
282-
});
283-
it('returns generated exercise if cache is high, cache lookup fails, but generation succeeds', async () => {
284-
vi.mocked(getValidatedExerciseFromCache).mockReturnValue(undefined);
285-
vi.mocked(generateAndValidateExercise).mockResolvedValue(mockExerciseContent);
286-
vi.mocked(saveExerciseToCache).mockReturnValue(555);
287-
const result = await getOrGenerateExercise(validGenParams, mockUserId, 200);
288-
expect(result.quizId).toBe(generatedSuccessData.id);
289-
expect(result.cached).toBe(false);
290-
expect(result.error).toBeNull();
291-
expect(result.quizData.paragraph).toBe(mockExerciseContent.paragraph);
291+
expect(GenerateExerciseResultSchema.safeParse(result).success).toBe(true);
292292
});
293293
it('returns generation error if cache is high, cache lookup and generation both fail', async () => {
294294
vi.mocked(getValidatedExerciseFromCache).mockReturnValue(undefined);
295295
vi.mocked(generateAndValidateExercise).mockRejectedValue(new Error('Generation Failed'));
296296
const result = await getOrGenerateExercise(validGenParams, mockUserId, 200);
297297
expect(result.error).toContain('Generation Failed');
298298
expect(result.quizId).toBe(-1);
299+
expect(GenerateExerciseResultSchema.safeParse(result).success).toBe(true);
299300
});
300301
});
301302
});

0 commit comments

Comments
 (0)