Finanzen-Anfrage: Dateien lesen (as: 'blob'), z. B. das PDF einer Auftragsbestätigung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d2883018fc
commit
eaf9fce1bc
2 files changed
+83
-2
No files matched your search
@@ -9,13 +9,14 @@ export const SESSION_EXPIRED_TEXT = 'Anmeldung abgelaufen. Bitte neu anmelden.'
|
|||||||
const statusError = (message, status) => Object.assign(new Error(message), { status })
|
const statusError = (message, status) => Object.assign(new Error(message), { status })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* request(path, { method, body }) -> geparstes JSON oder ein Error mit `status` (und bei 502 nach dem Festschreiben
|
* request(path, { method, body, as }) -> geparstes JSON oder ein Error mit `status` (und bei 502 nach dem Festschreiben
|
||||||
* `invoiceId` / `invoiceNumber`). Meldungen:
|
* `invoiceId` / `invoiceNumber`). Meldungen:
|
||||||
* - fetch wirft (Netz, Portal aus): „Das Kundenportal ist gerade nicht erreichbar.“ (status 0)
|
* - fetch wirft (Netz, Portal aus): „Das Kundenportal ist gerade nicht erreichbar.“ (status 0)
|
||||||
* - 502/503/504 ohne JSON-Antwort (Proxy-Fehlerseite): dieselbe Meldung (status = HTTP-Status)
|
* - 502/503/504 ohne JSON-Antwort (Proxy-Fehlerseite): dieselbe Meldung (status = HTTP-Status)
|
||||||
* - createJWT scheitert (Sitzung abgelaufen): „Anmeldung abgelaufen. Bitte neu anmelden.“ (status 401)
|
* - createJWT scheitert (Sitzung abgelaufen): „Anmeldung abgelaufen. Bitte neu anmelden.“ (status 401)
|
||||||
* - sonst die Meldung des Portals (`error`), ohne sie „API-Fehler <Status>“
|
* - sonst die Meldung des Portals (`error`), ohne sie „API-Fehler <Status>“
|
||||||
* Das JWT wird 10 Minuten gecacht; bei 401 einmal mit frischem JWT wiederholt.
|
* Das JWT wird 10 Minuten gecacht; bei 401 einmal mit frischem JWT wiederholt.
|
||||||
|
* Mit `as: 'blob'` liefert eine erfolgreiche Antwort den rohen Inhalt (Blob); Fehler kommen weiter als Meldung des Portals.
|
||||||
*/
|
*/
|
||||||
export function createFinanceRequest({ baseUrl, createJWT, fetchImpl = (...args) => globalThis.fetch(...args), now }) {
|
export function createFinanceRequest({ baseUrl, createJWT, fetchImpl = (...args) => globalThis.fetch(...args), now }) {
|
||||||
// JWT 10 Min. gecacht (Appwrite begrenzt das Erzeugen pro Nutzer)
|
// JWT 10 Min. gecacht (Appwrite begrenzt das Erzeugen pro Nutzer)
|
||||||
@@ -39,7 +40,7 @@ export function createFinanceRequest({ baseUrl, createJWT, fetchImpl = (...args)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return async function request(path, { method = 'GET', body } = {}) {
|
return async function request(path, { method = 'GET', body, as } = {}) {
|
||||||
const res = await fetchWithRetry(jwtCache, (jwt) =>
|
const res = await fetchWithRetry(jwtCache, (jwt) =>
|
||||||
portalFetch(`${baseUrl}${path}`, {
|
portalFetch(`${baseUrl}${path}`, {
|
||||||
method,
|
method,
|
||||||
@@ -47,6 +48,8 @@ export function createFinanceRequest({ baseUrl, createJWT, fetchImpl = (...args)
|
|||||||
body: body === undefined ? undefined : JSON.stringify(body),
|
body: body === undefined ? undefined : JSON.stringify(body),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
// Dateien (z. B. das PDF einer Auftragsbestätigung) kommen roh; Fehlerantworten bleiben JSON mit Meldung
|
||||||
|
if (as === 'blob' && res.ok) return res.blob()
|
||||||
let data = null
|
let data = null
|
||||||
try {
|
try {
|
||||||
data = await res.json()
|
data = await res.json()
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { loadModule } from './load.mjs'
|
||||||
|
|
||||||
|
const { createFinanceRequest, PORTAL_DOWN_TEXT } = await loadModule('src/lib/financeRequest.js')
|
||||||
|
|
||||||
|
const BASE = 'https://portal.example.invalid/api/admin/finanzen'
|
||||||
|
|
||||||
|
/** Antwort wie fetch: { ok, status, json(), blob() }; ohne `json` ist der Körper kein JSON (json() wirft). */
|
||||||
|
const reply = (status, { json, blob } = {}) => ({
|
||||||
|
ok: status >= 200 && status < 300,
|
||||||
|
status,
|
||||||
|
json: async () => {
|
||||||
|
if (json === undefined) throw new SyntaxError('Unexpected token % in JSON')
|
||||||
|
return json
|
||||||
|
},
|
||||||
|
blob: async () => blob,
|
||||||
|
})
|
||||||
|
|
||||||
|
function client({ responses = [], createJWT } = {}) {
|
||||||
|
const calls = []
|
||||||
|
const queue = [...responses]
|
||||||
|
const api = createFinanceRequest({
|
||||||
|
baseUrl: BASE,
|
||||||
|
createJWT: createJWT || (async () => ({ jwt: 'jwt-1' })),
|
||||||
|
fetchImpl: async (url, init) => {
|
||||||
|
calls.push({ url, init })
|
||||||
|
return queue.shift() || reply(200, { json: {} })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return { api, calls }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fails = async (promise) => promise.then(() => assert.fail('hätte scheitern müssen'), (err) => err)
|
||||||
|
const pdf = () => new Blob(['%PDF-1.7 Probe'], { type: 'application/pdf' })
|
||||||
|
|
||||||
|
test('as blob: Erfolg liefert den rohen Inhalt als Blob, mit Bearer-JWT, ohne JSON zu lesen', async () => {
|
||||||
|
const { api, calls } = client({
|
||||||
|
responses: [{ ...reply(200, { blob: pdf() }), json: async () => assert.fail('json() darf bei as blob nicht gelesen werden') }],
|
||||||
|
})
|
||||||
|
const result = await api('/auftraege/a1/pdf', { as: 'blob' })
|
||||||
|
assert.ok(result instanceof Blob)
|
||||||
|
assert.equal(result.type, 'application/pdf')
|
||||||
|
assert.equal(await result.text(), '%PDF-1.7 Probe')
|
||||||
|
assert.equal(calls[0].url, `${BASE}/auftraege/a1/pdf`)
|
||||||
|
assert.equal(calls[0].init.method, 'GET')
|
||||||
|
assert.equal(calls[0].init.headers.Authorization, 'Bearer jwt-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('as blob: Fehler kommen weiter als Meldung des Portals mit Status (kein kaputtes PDF)', async () => {
|
||||||
|
const { api } = client({ responses: [reply(404, { json: { error: 'PDF nicht gefunden.' } })] })
|
||||||
|
const err = await fails(api('/auftraege/a1/pdf', { as: 'blob' }))
|
||||||
|
assert.equal(err.message, 'PDF nicht gefunden.')
|
||||||
|
assert.equal(err.status, 404)
|
||||||
|
// Proxy-Fehlerseite ohne JSON: dieselbe Meldung wie bei JSON-Anfragen
|
||||||
|
const { api: api2 } = client({ responses: [reply(502)] })
|
||||||
|
assert.equal((await fails(api2('/auftraege/a1/pdf', { as: 'blob' }))).message, PORTAL_DOWN_TEXT)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('as blob: bei 401 einmal mit frischem JWT wiederholt, dann kommt der Blob', async () => {
|
||||||
|
let n = 0
|
||||||
|
const datei = pdf()
|
||||||
|
const { api, calls } = client({
|
||||||
|
createJWT: async () => ({ jwt: `jwt-${++n}` }),
|
||||||
|
responses: [reply(401, { json: { error: 'abgelaufen' } }), reply(200, { blob: datei })],
|
||||||
|
})
|
||||||
|
assert.equal(await api('/auftraege/a1/pdf', { as: 'blob' }), datei)
|
||||||
|
assert.deepEqual(calls.map((c) => c.init.headers.Authorization), ['Bearer jwt-1', 'Bearer jwt-2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ohne as bleibt es JSON (der Blob wird nicht gelesen), ein unbekannter Wert verhält sich ebenso', async () => {
|
||||||
|
const { api } = client({
|
||||||
|
responses: [{ ...reply(200, { json: { ok: 1 } }), blob: async () => assert.fail('blob() darf ohne as nicht gelesen werden') }],
|
||||||
|
})
|
||||||
|
assert.deepEqual(await api('/x'), { ok: 1 })
|
||||||
|
const { api: api2 } = client({ responses: [reply(200, { json: { ok: 2 } })] })
|
||||||
|
assert.deepEqual(await api2('/x', { as: 'text' }), { ok: 2 })
|
||||||
|
})
|
||||||
Reference in new issue
Block a user