174 lines
5.6 KiB
JavaScript
174 lines
5.6 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import vm from 'node:vm'
|
|
|
|
// public/kundenansicht.js im Browser-Nachbau: Kennung X-Kundenansicht an jede eigene /api-Anfrage,
|
|
// Hinweis-Overlay bei ersetzter (409 view_replaced) oder abgelaufener (401 view_expired) Ansicht
|
|
const SRC = readFileSync(new URL('../public/kundenansicht.js', import.meta.url), 'utf8')
|
|
const ID = 'a'.repeat(32)
|
|
|
|
function fakeElement(tag) {
|
|
return {
|
|
tag,
|
|
id: '',
|
|
className: '',
|
|
textContent: '',
|
|
type: '',
|
|
children: [],
|
|
attrs: {},
|
|
listeners: {},
|
|
appendChild(child) {
|
|
this.children.push(child)
|
|
return child
|
|
},
|
|
setAttribute(name, value) {
|
|
this.attrs[name] = value
|
|
},
|
|
addEventListener(name, fn) {
|
|
this.listeners[name] = fn
|
|
},
|
|
}
|
|
}
|
|
|
|
function browser({ respond = () => ({ status: 200, body: {} }) } = {}) {
|
|
const calls = []
|
|
const body = fakeElement('body')
|
|
const all = () => {
|
|
const out = []
|
|
const walk = (n) => {
|
|
out.push(n)
|
|
n.children.forEach(walk)
|
|
}
|
|
walk(body)
|
|
return out
|
|
}
|
|
const document = {
|
|
body,
|
|
createElement: (tag) => fakeElement(tag),
|
|
getElementById: (id) => all().find((n) => n.id === id) || null,
|
|
}
|
|
const xhrs = []
|
|
class XMLHttpRequest {
|
|
constructor() {
|
|
this.headers = {}
|
|
this.listeners = {}
|
|
xhrs.push(this)
|
|
}
|
|
open(method, url) {
|
|
this.method = method
|
|
this.url = url
|
|
}
|
|
setRequestHeader(name, value) {
|
|
this.headers[name] = value
|
|
}
|
|
addEventListener(name, fn) {
|
|
this.listeners[name] = fn
|
|
}
|
|
send() {
|
|
this.sent = true
|
|
}
|
|
}
|
|
const ctx = {
|
|
URL,
|
|
Headers,
|
|
Response,
|
|
console,
|
|
document,
|
|
XMLHttpRequest,
|
|
location: { href: 'https://project.webklar.com/dashboard.html', origin: 'https://project.webklar.com' },
|
|
close: () => {
|
|
ctx.closed = true
|
|
},
|
|
fetch: async (input, init = {}) => {
|
|
calls.push({ url: typeof input === 'string' ? input : input.url, headers: new Headers(init.headers || {}) })
|
|
const { status, body: json } = respond(input)
|
|
return new Response(JSON.stringify(json), { status, headers: { 'content-type': 'application/json' } })
|
|
},
|
|
}
|
|
ctx.window = ctx
|
|
vm.createContext(ctx)
|
|
vm.runInContext(SRC, ctx)
|
|
const overlayText = () => {
|
|
const box = document.getElementById('kb-view-overlay')
|
|
if (!box) return null
|
|
const texts = []
|
|
const walk = (n) => {
|
|
if (n.textContent) texts.push(n.textContent)
|
|
n.children.forEach(walk)
|
|
}
|
|
walk(box)
|
|
return texts.join(' | ')
|
|
}
|
|
return { ctx, calls, xhrs, overlayText }
|
|
}
|
|
|
|
const settle = () => new Promise((resolve) => setTimeout(resolve, 10))
|
|
|
|
test('ohne ID kein Header', async () => {
|
|
const { ctx, calls } = browser()
|
|
await ctx.fetch('/api/projects')
|
|
assert.equal(calls[0].headers.get('x-kundenansicht'), null)
|
|
})
|
|
|
|
test('mit ID: Header bei /api/... gleicher Herkunft, nicht bei fremder Herkunft oder /img', async () => {
|
|
const { ctx, calls } = browser()
|
|
ctx.KbView.setId(ID)
|
|
assert.equal(ctx.KbView.id(), ID)
|
|
await ctx.fetch('/api/projects')
|
|
await ctx.fetch('https://example.org/api/x')
|
|
await ctx.fetch('/img/logo.png')
|
|
await ctx.fetch('https://project.webklar.com/api/news', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
|
|
assert.equal(calls[0].headers.get('x-kundenansicht'), ID)
|
|
assert.equal(calls[1].headers.get('x-kundenansicht'), null)
|
|
assert.equal(calls[2].headers.get('x-kundenansicht'), null)
|
|
assert.equal(calls[3].headers.get('x-kundenansicht'), ID)
|
|
assert.equal(calls[3].headers.get('content-type'), 'application/json', 'vorhandene Header bleiben')
|
|
})
|
|
|
|
test('409 view_replaced zeigt Overlay mit Kundennamen, Antwort bleibt lesbar', async () => {
|
|
const { ctx, overlayText } = browser({ respond: () => ({ status: 409, body: { code: 'view_replaced', kunde: 'Kunde Zwei', error: 'x' } }) })
|
|
ctx.KbView.setId(ID)
|
|
const res = await ctx.fetch('/api/projects')
|
|
assert.equal((await res.json()).code, 'view_replaced')
|
|
await settle()
|
|
assert.match(overlayText(), /Diese Ansicht wurde durch die Kundenansicht von Kunde Zwei ersetzt\./)
|
|
assert.match(overlayText(), /Tab schließen/)
|
|
})
|
|
|
|
test('401 view_expired zeigt Abgelaufen-Overlay, andere 401 nicht', async () => {
|
|
const other = browser({ respond: () => ({ status: 401, body: { error: 'Nicht angemeldet' } }) })
|
|
await other.ctx.fetch('/api/projects')
|
|
await settle()
|
|
assert.equal(other.overlayText(), null)
|
|
const { ctx, overlayText } = browser({ respond: () => ({ status: 401, body: { code: 'view_expired' } }) })
|
|
await ctx.fetch('/api/projects')
|
|
await settle()
|
|
assert.match(overlayText(), /Die Kundenansicht ist abgelaufen\. Bitte im Ticketsystem erneut öffnen\./)
|
|
})
|
|
|
|
test('Overlay-Knopf schliesst den Tab, zweiter Hinweis ersetzt den Text statt zu stapeln', async () => {
|
|
const { ctx, overlayText } = browser()
|
|
ctx.KbView.showReplaced('Kunde Zwei')
|
|
ctx.KbView.showExpired()
|
|
const box = ctx.document.getElementById('kb-view-overlay')
|
|
assert.equal(ctx.document.body.children.length, 1)
|
|
assert.match(overlayText(), /abgelaufen/)
|
|
const find = (n) => (n.tag === 'button' ? n : n.children.map(find).find(Boolean))
|
|
find(box).listeners.click()
|
|
assert.equal(ctx.closed, true)
|
|
})
|
|
|
|
test('XHR bekommt den Header ebenfalls (nur eigene /api-Adressen)', () => {
|
|
const { ctx, xhrs } = browser()
|
|
ctx.KbView.setId(ID)
|
|
const a = new ctx.XMLHttpRequest()
|
|
a.open('POST', '/api/chat/attachments')
|
|
a.send()
|
|
const b = new ctx.XMLHttpRequest()
|
|
b.open('GET', 'https://example.org/api/x')
|
|
b.send()
|
|
assert.equal(xhrs[0].headers['X-Kundenansicht'], ID)
|
|
assert.equal(xhrs[1].headers['X-Kundenansicht'], undefined)
|
|
})
|