feat(simulations): GET /simulations/:id — lecture rapport avec auth + REPORT_NOT_READY
This commit is contained in:
parent
6ca2412304
commit
0680a6382f
3 changed files with 215 additions and 0 deletions
|
|
@ -64,6 +64,17 @@ function mockUpdate() {
|
||||||
} as any)
|
} as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mock from('productions').select(...).eq(...).single() pour getById */
|
||||||
|
function mockProductionSelect(data: unknown, error: unknown = null) {
|
||||||
|
vi.mocked(supabase.from).mockReturnValueOnce({
|
||||||
|
select: vi.fn(() => ({
|
||||||
|
eq: vi.fn(() => ({
|
||||||
|
single: vi.fn(() => ({ data, error })),
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
|
||||||
function createApp() {
|
function createApp() {
|
||||||
const app = new Hono<{ Variables: AppVariables }>()
|
const app = new Hono<{ Variables: AppVariables }>()
|
||||||
app.route('/simulations', simulationsRoutes)
|
app.route('/simulations', simulationsRoutes)
|
||||||
|
|
@ -193,3 +204,126 @@ describe('POST /simulations', () => {
|
||||||
expect(body.code).toBe('VALIDATION_ERROR')
|
expect(body.code).toBe('VALIDATION_ERROR')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('GET /simulations/:id', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const VALID_RAPPORT = {
|
||||||
|
score: 14,
|
||||||
|
nclc: 8,
|
||||||
|
feedback_court: 'Bonne production générale.',
|
||||||
|
criteres: [
|
||||||
|
{ nom: 'Coherence et cohesion', score: 4, commentaire: 'OK' },
|
||||||
|
{ nom: 'Lexique', score: 3, commentaire: 'OK' },
|
||||||
|
{ nom: 'Morphosyntaxe', score: 4, commentaire: 'OK' },
|
||||||
|
{ nom: 'Pertinence', score: 3, commentaire: 'OK' },
|
||||||
|
],
|
||||||
|
erreurs: ['erreur 1'],
|
||||||
|
modele: 'Texte modèle.',
|
||||||
|
idees: ['idée 1'],
|
||||||
|
exercices: ['exo 1'],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('OK : rapport trouvé et appartenant à l\'utilisateur → 200 avec simulation_id', async () => {
|
||||||
|
const profile = buildProfile({ id: 'user-123', plan: 'standard' })
|
||||||
|
mockAuth(profile)
|
||||||
|
mockProductionSelect({
|
||||||
|
id: 'prod-42',
|
||||||
|
user_id: 'user-123',
|
||||||
|
tache: 'EE_T1',
|
||||||
|
mode: 'entrainement',
|
||||||
|
score: 14,
|
||||||
|
nclc: 8,
|
||||||
|
rapport: JSON.stringify(VALID_RAPPORT),
|
||||||
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp()
|
||||||
|
const res = await app.request('/simulations/prod-42', {
|
||||||
|
headers: { Authorization: 'Bearer token' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body.simulation_id).toBe('prod-42')
|
||||||
|
expect(body.tache).toBe('EE_T1')
|
||||||
|
expect(body.mode).toBe('entrainement')
|
||||||
|
expect(body.created_at).toBe('2024-01-01T00:00:00Z')
|
||||||
|
expect(body.score).toBe(14)
|
||||||
|
expect(body.nclc).toBe(8)
|
||||||
|
expect(body.feedback_court).toBe('Bonne production générale.')
|
||||||
|
expect(body.criteres).toHaveLength(4)
|
||||||
|
expect(body.erreurs).toEqual(['erreur 1'])
|
||||||
|
expect(body.modele).toBe('Texte modèle.')
|
||||||
|
expect(body.idees).toEqual(['idée 1'])
|
||||||
|
expect(body.exercices).toEqual(['exo 1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('SIMULATION_NOT_FOUND : id inexistant → 404', async () => {
|
||||||
|
const profile = buildProfile({ id: 'user-123' })
|
||||||
|
mockAuth(profile)
|
||||||
|
mockProductionSelect(null, { message: 'No rows returned' })
|
||||||
|
|
||||||
|
const app = createApp()
|
||||||
|
const res = await app.request('/simulations/does-not-exist', {
|
||||||
|
headers: { Authorization: 'Bearer token' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(404)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body.error).toBe(true)
|
||||||
|
expect(body.code).toBe('SIMULATION_NOT_FOUND')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('AUTH_REQUIRED : user_id !== profile.id → 401', async () => {
|
||||||
|
const profile = buildProfile({ id: 'user-123' })
|
||||||
|
mockAuth(profile)
|
||||||
|
mockProductionSelect({
|
||||||
|
id: 'prod-42',
|
||||||
|
user_id: 'another-user-456',
|
||||||
|
tache: 'EE_T1',
|
||||||
|
mode: 'entrainement',
|
||||||
|
score: 14,
|
||||||
|
nclc: 8,
|
||||||
|
rapport: JSON.stringify(VALID_RAPPORT),
|
||||||
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp()
|
||||||
|
const res = await app.request('/simulations/prod-42', {
|
||||||
|
headers: { Authorization: 'Bearer token' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(401)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body.error).toBe(true)
|
||||||
|
expect(body.code).toBe('AUTH_REQUIRED')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('REPORT_NOT_READY : rapport === null → 404', async () => {
|
||||||
|
const profile = buildProfile({ id: 'user-123' })
|
||||||
|
mockAuth(profile)
|
||||||
|
mockProductionSelect({
|
||||||
|
id: 'prod-42',
|
||||||
|
user_id: 'user-123',
|
||||||
|
tache: 'EE_T1',
|
||||||
|
mode: 'entrainement',
|
||||||
|
score: null,
|
||||||
|
nclc: null,
|
||||||
|
rapport: null,
|
||||||
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp()
|
||||||
|
const res = await app.request('/simulations/prod-42', {
|
||||||
|
headers: { Authorization: 'Bearer token' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(404)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body.error).toBe(true)
|
||||||
|
expect(body.code).toBe('REPORT_NOT_READY')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { supabase } from '../lib/supabase.js'
|
import { supabase } from '../lib/supabase.js'
|
||||||
import { canUserSimulate, getPlanPermissions } from '../lib/access.js'
|
import { canUserSimulate, getPlanPermissions } from '../lib/access.js'
|
||||||
import type { Plan } from '../lib/access.js'
|
import type { Plan } from '../lib/access.js'
|
||||||
|
import type { EERapport } from '../lib/deepseek.js'
|
||||||
import type { AuthProfile } from '../middleware/auth.js'
|
import type { AuthProfile } from '../middleware/auth.js'
|
||||||
|
|
||||||
export type Tache = 'EE_T1' | 'EE_T2' | 'EE_T3' | 'EO_T1' | 'EO_T3' | 'EO_T2_LIVE'
|
export type Tache = 'EE_T1' | 'EE_T2' | 'EE_T3' | 'EO_T1' | 'EO_T3' | 'EO_T2_LIVE'
|
||||||
|
|
@ -74,3 +75,69 @@ export async function create(
|
||||||
|
|
||||||
return { data: data as CreateResult }
|
return { data: data as CreateResult }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EERapport et EORapport ont la même structure depuis l'étape A —
|
||||||
|
// on utilise EERapport comme représentation canonique du rapport parsé.
|
||||||
|
export type GetByIdResult = EERapport & {
|
||||||
|
simulation_id: string
|
||||||
|
tache: Tache
|
||||||
|
mode: Mode
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetByIdError = {
|
||||||
|
error: true
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getById(
|
||||||
|
id: string,
|
||||||
|
profile: AuthProfile
|
||||||
|
): Promise<{ data: GetByIdResult } | GetByIdError> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('productions')
|
||||||
|
.select('id, user_id, tache, mode, score, nclc, rapport, created_at')
|
||||||
|
.eq('id', id)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
code: 'SIMULATION_NOT_FOUND',
|
||||||
|
message: 'Simulation introuvable.',
|
||||||
|
status: 404,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.user_id !== profile.id) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
code: 'AUTH_REQUIRED',
|
||||||
|
message: 'Cette simulation ne vous appartient pas.',
|
||||||
|
status: 401,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.rapport === null) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
code: 'REPORT_NOT_READY',
|
||||||
|
message: "Le rapport n'est pas encore disponible pour cette simulation.",
|
||||||
|
status: 404,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rapport = JSON.parse(data.rapport) as EERapport
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
...rapport,
|
||||||
|
simulation_id: data.id,
|
||||||
|
tache: data.tache as Tache,
|
||||||
|
mode: data.mode as Mode,
|
||||||
|
created_at: data.created_at,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,4 +78,18 @@ simulations.post('/', authMiddleware, async (c) => {
|
||||||
return c.json(result.data, 201)
|
return c.json(result.data, 201)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
simulations.get('/:id', authMiddleware, async (c) => {
|
||||||
|
// `:id` est garanti présent par le pattern de route Hono
|
||||||
|
const id = c.req.param('id')!
|
||||||
|
const profile = c.get('profile')
|
||||||
|
|
||||||
|
const result = await simulationController.getById(id, profile)
|
||||||
|
|
||||||
|
if ('error' in result) {
|
||||||
|
return c.json(result, result.status as 401 | 404 | 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result.data, 200)
|
||||||
|
})
|
||||||
|
|
||||||
export default simulations
|
export default simulations
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue