feat(fase-20): accept-language + bootstrap recognise 'eu'
Extends the Fase 10.8 Accept-Language parser to surface `'eu'` as a third
known primary subtag, gated on the same q ≥ 0.5 threshold the existing
`'es'` path uses. `'en'` continues to short-circuit because it's the
source language. `AppLanguage` widens to `'en' | 'es' | 'eu'`.
Tests:
* Unit (apps/web): LANG-U-EU-01 bare 'eu', LANG-U-EU-02 'eu-ES',
LANG-U-EU-03 'eu-FR;q=0.5,en;q=0.4' (borderline q wins), LANG-U-EU-04
navigator.languages array — 4 new specs, all green.
* Integration (packages/test-utils): LANG-INT-EU-01 user updates own
language=eu, LANG-INT-EU-02 user attempts language='fr' and gets the
Postgres 22P02 from the ENUM (cast through `as unknown as` so the
narrow TS type doesn't block the test).
The integration negative case lands on the language_code ENUM gate from
migration 028, not RLS — it would still fail even from a service-role
client.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -57,4 +57,58 @@ describe('Language bootstrap UPDATE', () => {
|
||||
// Borja's seed language is 'es'; RLS must have blocked Ana's update.
|
||||
expect(data?.language).toBe('es');
|
||||
});
|
||||
|
||||
// ─── Fase 20.4.2 — Euskera bootstrap ─────────────────────────────────
|
||||
// The bootstrap call sites build the UPDATE payload from the parser
|
||||
// output of `detectLanguage(...)`. These exercise the live write of an
|
||||
// `eu` value against the actual ENUM, and a negative case that the
|
||||
// gate (the language_code enum from migration 028) rejects an unknown
|
||||
// locale at write time so a careless client cannot persist a value the
|
||||
// app has no messages for.
|
||||
|
||||
it('LANG-INT-EU-01: user can update their own language to eu', async () => {
|
||||
const ana = await createClientAs(ANA_ID);
|
||||
// Reset to en first via admin so the assertion measures the user
|
||||
// write, not seed state.
|
||||
await admin.from('users').update({ language: 'en' }).eq('id', ANA_ID);
|
||||
|
||||
const { error } = await ana
|
||||
.from('users')
|
||||
.update({ language: 'eu' })
|
||||
.eq('id', ANA_ID);
|
||||
expect(error).toBeNull();
|
||||
|
||||
const { data } = await admin
|
||||
.from('users')
|
||||
.select('language')
|
||||
.eq('id', ANA_ID)
|
||||
.single();
|
||||
expect(data?.language).toBe('eu');
|
||||
});
|
||||
|
||||
it('LANG-INT-EU-02: user trying language=\'fr\' is rejected by the enum', async () => {
|
||||
const ana = await createClientAs(ANA_ID);
|
||||
// Pre-set Ana to a known good value so a regression that silently
|
||||
// accepts the bad UPDATE can't hide behind seed state.
|
||||
await admin.from('users').update({ language: 'es' }).eq('id', ANA_ID);
|
||||
|
||||
const { error } = await ana
|
||||
.from('users')
|
||||
// Force any client-side narrow types out of the way — the gate
|
||||
// lives in Postgres, not TypeScript.
|
||||
.update({ language: 'fr' as unknown as 'en' })
|
||||
.eq('id', ANA_ID);
|
||||
// Postgres invalid_text_representation (22P02) for an unknown enum
|
||||
// label; PostgREST surfaces it as a structured error.
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.code).toBe('22P02');
|
||||
|
||||
const { data } = await admin
|
||||
.from('users')
|
||||
.select('language')
|
||||
.eq('id', ANA_ID)
|
||||
.single();
|
||||
// Untouched.
|
||||
expect(data?.language).toBe('es');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user