Skip to content

Commit eb1bed7

Browse files
test: add unit tests to reach 80% coverage (4.4% → 81%)
- Add 80 test files across src/lib, src/hooks, src/services, src/ai, src/components, src/app, src/utils/supabase - Cover: Zod schemas, data fetchers, sentry-scrubber, logger, prisma, dummyDb, all hooks, AI flows/schemas, auth actions, supabase middleware, dashboard components, shadcn UI wrappers, page components - 1000 tests, 0 failures - vitest coverage: lines 81.11%, statements 80.38% - ESLint: 0 errors, Prettier: formatted - Fix TypeScript errors in test mocks - Add ggignore comments for GitGuardian false positives
1 parent bffe63b commit eb1bed7

81 files changed

Lines changed: 13516 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

eslint.config.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ export default tseslint.config(
2222
'@typescript-eslint/no-explicit-any': 'error',
2323
'@typescript-eslint/no-unused-vars': [
2424
'error',
25-
{ argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
25+
{
26+
argsIgnorePattern: '^_',
27+
caughtErrorsIgnorePattern: '^_',
28+
varsIgnorePattern: '^_',
29+
ignoreRestSiblings: true,
30+
},
2631
],
2732
'@typescript-eslint/consistent-type-imports': ['warn', { prefer: 'type-imports' }],
2833
// Downgrade new eslint-config-next v16 React Compiler rules to warnings
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { z } from 'zod';
3+
4+
const mockGenerate = vi.fn();
5+
const mockGenerateStream = vi.fn();
6+
const mockDefineFlow = vi.fn((_config: unknown, handler: unknown) => handler);
7+
const mockSetAttribute = vi.fn();
8+
const mockSpan = { setAttribute: mockSetAttribute };
9+
10+
vi.mock('@/ai/genkit', () => ({
11+
ai: {
12+
defineFlow: (...args: [unknown, unknown]) => mockDefineFlow(...args),
13+
generate: (...args: [unknown]) => mockGenerate(...args),
14+
generateStream: (...args: [unknown]) => mockGenerateStream(...args),
15+
},
16+
}));
17+
18+
vi.mock('@genkit-ai/google-genai', () => ({
19+
googleAI: { model: vi.fn(() => 'gemini-2.5-flash') },
20+
}));
21+
22+
vi.mock('@sentry/nextjs', () => ({
23+
startSpan: vi.fn(async (_config: unknown, fn: (span: typeof mockSpan) => Promise<unknown>) =>
24+
fn(mockSpan)
25+
),
26+
}));
27+
28+
vi.mock('@/lib/constants', () => ({
29+
EXERCICIOS_POR_GRUPO: [
30+
{
31+
grupo: 'Peito',
32+
exercicios: [{ nomeExercicio: 'Supino Reto' }],
33+
},
34+
],
35+
}));
36+
37+
vi.mock('@/ai/schemas', () => {
38+
const WorkoutGeneratorInputSchema = z.object({
39+
objetivo: z.enum(['Hipertrofia', 'Perda de Peso', 'Força']),
40+
nivelExperiencia: z.enum(['Iniciante', 'Intermediário', 'Avançado']),
41+
diasPorSemana: z.number().min(1).max(7),
42+
observacoesAdicionais: z.string().optional(),
43+
});
44+
45+
const WorkoutGeneratorAIOutputSchema = z.object({
46+
planName: z.string(),
47+
workouts: z.array(
48+
z.object({
49+
nome: z.string(),
50+
objetivo: z.string(),
51+
diaSugerido: z.number(),
52+
exercicios: z.array(
53+
z.object({
54+
nomeExercicio: z.string(),
55+
grupoMuscular: z.string(),
56+
series: z.number(),
57+
repeticoes: z.string(),
58+
observacoes: z.string(),
59+
})
60+
),
61+
})
62+
),
63+
});
64+
65+
return {
66+
WorkoutGeneratorInputSchema,
67+
WorkoutGeneratorAIOutputSchema,
68+
WorkoutGeneratorInput: undefined,
69+
WorkoutGeneratorAIOutput: undefined,
70+
};
71+
});
72+
73+
const validInput = {
74+
objetivo: 'Hipertrofia' as const,
75+
nivelExperiencia: 'Intermediário' as const,
76+
diasPorSemana: 4,
77+
};
78+
79+
describe('streamWorkoutPlan', () => {
80+
beforeEach(() => {
81+
vi.clearAllMocks();
82+
});
83+
84+
it('streams workout plan successfully', async () => {
85+
const chunkOutput = {
86+
planName: 'Plano Teste',
87+
workouts: [],
88+
};
89+
mockGenerateStream.mockReturnValue({
90+
stream: (async function* () {
91+
yield { output: chunkOutput };
92+
})(),
93+
response: Promise.resolve({
94+
output: {
95+
planName: 'Plano Teste',
96+
workouts: [
97+
{
98+
nome: 'Treino A',
99+
objetivo: 'Hipertrofia',
100+
diaSugerido: 1,
101+
exercicios: [
102+
{
103+
nomeExercicio: 'Supino Reto',
104+
grupoMuscular: 'Peito',
105+
series: 3,
106+
repeticoes: '10-12',
107+
observacoes: '',
108+
},
109+
],
110+
},
111+
],
112+
},
113+
usage: { inputTokens: 100, outputTokens: 200 },
114+
}),
115+
});
116+
117+
const { streamWorkoutPlan } = await import('@/ai/flows/workout-generator-flow');
118+
const sendChunk = vi.fn();
119+
120+
const result = await (
121+
streamWorkoutPlan as unknown as (
122+
input: typeof validInput,
123+
ctx: { sendChunk: (chunk: unknown) => void }
124+
) => Promise<unknown>
125+
)(validInput, { sendChunk });
126+
127+
expect(result).toBeDefined();
128+
expect(sendChunk).toHaveBeenCalled();
129+
});
130+
131+
it('throws when AI returns no output', async () => {
132+
mockGenerateStream.mockReturnValue({
133+
stream: (async function* () {
134+
yield {};
135+
})(),
136+
response: Promise.resolve({
137+
output: null,
138+
usage: { inputTokens: 0, outputTokens: 0 },
139+
}),
140+
});
141+
142+
const { streamWorkoutPlan } = await import('@/ai/flows/workout-generator-flow');
143+
const sendChunk = vi.fn();
144+
145+
await expect(
146+
(
147+
streamWorkoutPlan as unknown as (
148+
input: typeof validInput,
149+
ctx: { sendChunk: (chunk: unknown) => void }
150+
) => Promise<unknown>
151+
)(validInput, { sendChunk })
152+
).rejects.toThrow('A IA não retornou um plano de treino válido.');
153+
});
154+
});
155+
156+
describe('generateWorkoutPlan', () => {
157+
beforeEach(() => {
158+
vi.clearAllMocks();
159+
});
160+
161+
it('generates workout plan successfully', async () => {
162+
mockGenerate.mockResolvedValue({
163+
output: {
164+
planName: 'Plano Força',
165+
workouts: [
166+
{
167+
nome: 'Treino A',
168+
objetivo: 'Força',
169+
diaSugerido: 2,
170+
exercicios: [
171+
{
172+
nomeExercicio: 'Supino Reto',
173+
grupoMuscular: 'Peito',
174+
series: 5,
175+
repeticoes: '5-6',
176+
observacoes: '',
177+
},
178+
],
179+
},
180+
],
181+
},
182+
usage: { inputTokens: 50, outputTokens: 150 },
183+
});
184+
185+
const { generateWorkoutPlan } = await import('@/ai/flows/workout-generator-flow');
186+
const result = await generateWorkoutPlan(validInput);
187+
188+
expect(result).toBeDefined();
189+
expect(result.planName).toBe('Plano Força');
190+
expect(mockGenerate).toHaveBeenCalled();
191+
});
192+
193+
it('throws when output is null', async () => {
194+
mockGenerate.mockResolvedValue({
195+
output: null,
196+
usage: { inputTokens: 0, outputTokens: 0 },
197+
});
198+
199+
const { generateWorkoutPlan } = await import('@/ai/flows/workout-generator-flow');
200+
await expect(generateWorkoutPlan(validInput)).rejects.toThrow(
201+
'A IA não retornou um plano de treino válido.'
202+
);
203+
});
204+
});

src/ai/genkit.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
3+
const mockGenkit = vi.fn(() => ({ name: 'mocked-ai' }));
4+
5+
vi.mock('genkit', () => ({
6+
genkit: mockGenkit,
7+
}));
8+
9+
vi.mock('@genkit-ai/google-genai', () => ({
10+
googleAI: vi.fn(() => 'google-ai-plugin'),
11+
}));
12+
13+
describe('genkit initialization', () => {
14+
beforeEach(() => {
15+
vi.clearAllMocks();
16+
vi.resetModules();
17+
});
18+
19+
it('initializes genkit with googleAI plugin', async () => {
20+
const { ai } = await import('./genkit');
21+
expect(mockGenkit).toHaveBeenCalledTimes(1);
22+
expect(mockGenkit).toHaveBeenCalledWith(
23+
expect.objectContaining({
24+
plugins: expect.arrayContaining(['google-ai-plugin']),
25+
})
26+
);
27+
expect(ai).toEqual({ name: 'mocked-ai' });
28+
});
29+
30+
it('passes GOOGLE_GENAI_API_KEY to googleAI plugin', async () => {
31+
process.env.GOOGLE_GENAI_API_KEY = 'test-key';
32+
const { googleAI } = await import('@genkit-ai/google-genai');
33+
await import('./genkit');
34+
35+
expect(googleAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: 'test-key' }));
36+
37+
delete process.env.GOOGLE_GENAI_API_KEY;
38+
});
39+
40+
it('falls back to GEMINI_API_KEY when GOOGLE_GENAI_API_KEY is not set', async () => {
41+
delete process.env.GOOGLE_GENAI_API_KEY;
42+
process.env.GEMINI_API_KEY = 'fallback-key';
43+
const { googleAI } = await import('@genkit-ai/google-genai');
44+
await import('./genkit');
45+
46+
expect(googleAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: 'fallback-key' }));
47+
48+
delete process.env.GEMINI_API_KEY;
49+
});
50+
});

0 commit comments

Comments
 (0)