feat(front) : util httpExternal + autocomplete adresse BAN (ERP-66) (#52)
Auto Tag Develop / tag (push) Successful in 7s
Auto Tag Develop / tag (push) Successful in 7s
## ERP-66 — Utilitaires adresse/téléphone + autocomplétion BAN ### feat - **httpExternal** : client dédié aux API publiques externes (URL absolue, sans cookie de session, timeout). Seul point d'entrée autorisé pour un `$fetch` externe (règle frontend n°4). - **useAddressAutocomplete** : implémentation BAN (api-adresse.data.gouv.fr) — recherche ville (`type=municipality`) et adresse, mapping GeoJSON, throw en cas d'erreur/timeout (mode dégradé côté composant). La recherche d'adresse n'impose **pas** `type=housenumber` (sinon 0 résultat tant qu'aucun numéro n'est saisi) — spec-front mise à jour. - Tests Vitest : httpExternal, useAddressAutocomplete, cas limites `formatPhoneFR`. ### fix - **ClientAddressBlock** : la rue courante est toujours réinjectée dans les options de `MalioInputAutocomplete` (computed, miroir de `cityOptions`). Corrige le champ Adresse qui se vidait après validation / à l'édition d'une adresse existante (valeur pourtant persistée). Test de montage ajouté. - **useClientReferentials** : libellé des sites = numéro de département (2 premiers chiffres du code postal, déjà exposé par `/sites`) au lieu du nom. ### Vérifs - ESLint ✅ · Vitest 196/196 ✅ - Changements 100% frontend (+ doc spec). Reviewed-on: #52 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #52.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { httpExternal } from '../httpExternal'
|
||||
|
||||
// On mocke ofetch : httpExternal s'appuie sur $fetch sans jamais toucher le
|
||||
// reseau pendant les tests. vi.mock est hoiste par Vitest au-dessus des imports.
|
||||
const mockFetch = vi.hoisted(() => vi.fn())
|
||||
vi.mock('ofetch', () => ({ $fetch: mockFetch }))
|
||||
|
||||
describe('httpExternal', () => {
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset()
|
||||
})
|
||||
|
||||
it('retourne le JSON parse renvoye par $fetch', async () => {
|
||||
mockFetch.mockResolvedValueOnce({ ok: true })
|
||||
|
||||
const res = await httpExternal<{ ok: boolean }>('https://example.test/api')
|
||||
|
||||
expect(res).toEqual({ ok: true })
|
||||
})
|
||||
|
||||
it('transmet la query, coupe le cookie (credentials omit) et pose un timeout par defaut', async () => {
|
||||
mockFetch.mockResolvedValueOnce([])
|
||||
|
||||
await httpExternal('https://example.test/search', {
|
||||
query: { q: '80000', type: 'municipality' },
|
||||
})
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://example.test/search',
|
||||
expect.objectContaining({
|
||||
query: { q: '80000', type: 'municipality' },
|
||||
credentials: 'omit',
|
||||
retry: 0,
|
||||
timeout: 5000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('permet de surcharger le timeout', async () => {
|
||||
mockFetch.mockResolvedValueOnce(null)
|
||||
|
||||
await httpExternal('https://example.test', { timeoutMs: 1000 })
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://example.test',
|
||||
expect.objectContaining({ timeout: 1000 }),
|
||||
)
|
||||
})
|
||||
|
||||
it('propage l\'erreur reseau / timeout (throw)', async () => {
|
||||
mockFetch.mockRejectedValueOnce(new Error('network down'))
|
||||
|
||||
await expect(httpExternal('https://example.test')).rejects.toThrow('network down')
|
||||
})
|
||||
})
|
||||
@@ -20,4 +20,27 @@ describe('formatPhoneFR', () => {
|
||||
it('groupe par 2 meme un nombre impair de chiffres (dernier groupe seul)', () => {
|
||||
expect(formatPhoneFR('123')).toBe('12 3')
|
||||
})
|
||||
|
||||
it('formate une saisie courte (<= 4 chiffres) sans planter', () => {
|
||||
expect(formatPhoneFR('1')).toBe('1')
|
||||
expect(formatPhoneFR('12')).toBe('12')
|
||||
expect(formatPhoneFR('1234')).toBe('12 34')
|
||||
})
|
||||
|
||||
it('strip les caracteres non numeriques (lettres, espaces, ponctuation)', () => {
|
||||
expect(formatPhoneFR('abc')).toBe('')
|
||||
expect(formatPhoneFR('Tel : 06.12')).toBe('06 12')
|
||||
expect(formatPhoneFR(' 06 12 ')).toBe('06 12')
|
||||
})
|
||||
|
||||
it('conserve l\'indicatif international (+33) sans le transformer', () => {
|
||||
// Comportement fige : on retire seulement le `+`, on ne deduit pas le
|
||||
// prefixe pays. Le `+33...` est donc groupe brut par paquets de 2.
|
||||
expect(formatPhoneFR('+33612345678')).toBe('33 61 23 45 67 8')
|
||||
})
|
||||
|
||||
it('groupe sans tronquer une saisie plus longue que 10 chiffres', () => {
|
||||
// Aucune troncature silencieuse : on figure tous les chiffres groupes par 2.
|
||||
expect(formatPhoneFR('061234567899')).toBe('06 12 34 56 78 99')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { $fetch } from 'ofetch'
|
||||
|
||||
/**
|
||||
* Options d'un appel HTTP externe.
|
||||
*/
|
||||
export interface HttpExternalOptions {
|
||||
/** Parametres de query string (encodes par ofetch). */
|
||||
query?: Record<string, string | number | undefined>
|
||||
/** Timeout en millisecondes avant abandon (defaut 5000). */
|
||||
timeoutMs?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Petit client HTTP pour les APIs PUBLIQUES EXTERNES (domaine tiers, hors `/api`).
|
||||
*
|
||||
* Pourquoi un helper dedie plutot que `useApi()` : `useApi()` est le client de
|
||||
* l'API interne Starseed (baseURL `/api`, cookie JWT `credentials: 'include'`,
|
||||
* parsing/erreurs Hydra, redirection `/login` sur 401, toasts i18n). Tout cela
|
||||
* est inadapte — voire indesirable — pour un endpoint public externe comme la
|
||||
* Base Adresse Nationale (`api-adresse.data.gouv.fr`).
|
||||
*
|
||||
* Ce helper est donc le SEUL point d'entree autorise pour un `$fetch` brut vers
|
||||
* l'externe (cf. regle frontend n°4 : pas de `$fetch` eparpille dans les
|
||||
* composants). Il :
|
||||
* - cible une URL absolue (pas de baseURL `/api`) ;
|
||||
* - n'envoie PAS le cookie de session (`credentials: 'omit'`) ;
|
||||
* - ne retente pas (`retry: 0`) et applique un timeout ;
|
||||
* - laisse remonter l'erreur (throw) — au consommateur de gerer le mode degrade.
|
||||
*/
|
||||
export async function httpExternal<T>(
|
||||
url: string,
|
||||
opts: HttpExternalOptions = {},
|
||||
): Promise<T> {
|
||||
return $fetch<T>(url, {
|
||||
query: opts.query,
|
||||
credentials: 'omit',
|
||||
retry: 0,
|
||||
timeout: opts.timeoutMs ?? 5000,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user