242 lines
13 KiB
JavaScript
242 lines
13 KiB
JavaScript
import { test, after } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
|
import { startFakeStripe } from './support/fakeStripe.js'
|
|
import { useTestEnv } from './support/testEnv.js'
|
|
|
|
// Zugang zum Ticketsystem nur mit Label (scripts/mitarbeiter-zugang.mjs, employeeAdmin.js)
|
|
const aw = await startAppwriteMock()
|
|
const st = await startFakeStripe()
|
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
|
const {
|
|
applyPermissionChanges,
|
|
grantStaffLabel,
|
|
planPermissionChanges,
|
|
removeEmployeeEntry,
|
|
staffAccessStatus,
|
|
staffOnlyPermissions,
|
|
} = await import('../server/services/staffAccessAdmin.js')
|
|
const { isStaffAccount, isAdminAccount } = await import('../server/services/staffLabels.js')
|
|
const { createEmployeeWithLogin, deleteEmployeeWithLogin, updateEmployeeWithLogin } = await import('../server/services/employeeAdmin.js')
|
|
after(() => Promise.all([aw.close(), st.close()]))
|
|
|
|
const STAFF = ['label:mitarbeiter', 'label:admin']
|
|
|
|
test('isStaffAccount: nur Label mitarbeiter oder admin, nie ohne Konto', () => {
|
|
assert.equal(isStaffAccount({ labels: ['mitarbeiter'] }), true)
|
|
assert.equal(isStaffAccount({ labels: ['admin'] }), true)
|
|
assert.equal(isStaffAccount({ labels: [] }), false)
|
|
assert.equal(isStaffAccount({ labels: ['kunde'] }), false)
|
|
assert.equal(isStaffAccount(null), false)
|
|
assert.equal(isAdminAccount({ labels: ['mitarbeiter'] }), false)
|
|
})
|
|
|
|
test('staffOnlyPermissions: any/guests/users -> Mitarbeiter-Labels, andere Rollen bleiben, keine Doppel', () => {
|
|
assert.deepEqual(
|
|
staffOnlyPermissions(['read("users")', 'create("users")', 'read("any")', 'update("guests")', 'read("user:abc")', 'delete("users/verified")']),
|
|
[
|
|
...STAFF.map((r) => `read("${r}")`),
|
|
...STAFF.map((r) => `create("${r}")`),
|
|
...STAFF.map((r) => `update("${r}")`),
|
|
'read("user:abc")',
|
|
...STAFF.map((r) => `delete("${r}")`),
|
|
]
|
|
)
|
|
assert.deepEqual(staffOnlyPermissions(['read("label:mitarbeiter")', 'read("team:x")']), ['read("label:mitarbeiter")', 'read("team:x")'])
|
|
assert.deepEqual(staffOnlyPermissions([]), [])
|
|
assert.deepEqual(staffOnlyPermissions(['write("users")', 'read("users/unverified")']), [
|
|
...STAFF.map((r) => `write("${r}")`),
|
|
...STAFF.map((r) => `read("${r}")`),
|
|
])
|
|
})
|
|
|
|
test('staffOnlyPermissions: zweiter Lauf aendert nichts mehr', () => {
|
|
const once = staffOnlyPermissions(['read("users")', 'create("any")', 'delete("user:u1")'])
|
|
assert.deepEqual(staffOnlyPermissions(once), once)
|
|
})
|
|
|
|
// Nachbau der Appwrite-Verwaltungs-API fuer Rechte (Datenbanken, Collections, Dokumente, Buckets, Dateien)
|
|
function fakeAdminApi() {
|
|
const collections = {
|
|
tickets: { $id: 'workorders', name: 'Work Orders', documentSecurity: false, enabled: true, $permissions: ['read("users")', 'update("users")'] },
|
|
portal: { $id: 'chatMessages', name: 'Chat', documentSecurity: false, enabled: true, $permissions: [] },
|
|
docs: { $id: 'notes', name: 'Notes', documentSecurity: true, enabled: true, $permissions: ['read("label:mitarbeiter")'] },
|
|
}
|
|
const documents = [
|
|
{ $id: 'n1', $permissions: ['read("user:u1")'] },
|
|
{ $id: 'n2', $permissions: ['read("any")'] },
|
|
]
|
|
const bucket = {
|
|
$id: 'legal-files', name: 'Legal', fileSecurity: false, enabled: true, maximumFileSize: 30000000,
|
|
allowedFileExtensions: ['pdf'], compression: 'none', encryption: true, antivirus: true, $permissions: ['read("users")', 'create("users")'],
|
|
}
|
|
const writes = []
|
|
const request = async (path, { method = 'GET', body, queries = [] } = {}) => {
|
|
if (method !== 'GET') {
|
|
if (path.endsWith('/collections/workorders') && body.name === undefined) throw new Error('name fehlt')
|
|
if (path.startsWith('/storage/') && method === 'PUT' && path.endsWith('/broken')) throw new Error('missing scope (buckets.write)')
|
|
writes.push({ method, path, body })
|
|
return {}
|
|
}
|
|
if (path === '/databases') return { databases: [{ $id: 'woms-database' }] }
|
|
if (path === '/databases/woms-database/collections') return { collections: Object.values(collections) }
|
|
if (path === '/databases/woms-database/collections/notes/documents') {
|
|
// Seiten zu 100 per cursorAfter wie Appwrite
|
|
const cursor = queries.map((q) => (typeof q === 'string' ? JSON.parse(q) : q)).find((q) => q.method === 'cursorAfter')?.values[0]
|
|
const start = cursor ? documents.findIndex((d) => d.$id === cursor) + 1 : 0
|
|
return { documents: documents.slice(start, start + 100) }
|
|
}
|
|
if (path === '/storage/buckets') return { buckets: [bucket, { ...bucket, $id: 'broken', name: 'Kaputt' }] }
|
|
throw new Error(`unerwartet ${path}`)
|
|
}
|
|
return { request, writes, documents }
|
|
}
|
|
|
|
test('planPermissionChanges: offene Collections, Dokumente und Buckets, geschlossene nicht', async () => {
|
|
const api = fakeAdminApi()
|
|
const { changes, errors } = await planPermissionChanges(api.request)
|
|
assert.deepEqual(errors, [])
|
|
assert.deepEqual(changes.map((c) => `${c.kind}:${c.id}`), ['collection:workorders', 'document:n2', 'bucket:legal-files', 'bucket:broken'])
|
|
const tickets = changes[0]
|
|
assert.deepEqual(tickets.before, ['read("users")', 'update("users")'])
|
|
assert.deepEqual(tickets.after, [...STAFF.map((r) => `read("${r}")`), ...STAFF.map((r) => `update("${r}")`)])
|
|
assert.deepEqual(tickets.settings, { name: 'Work Orders', documentSecurity: false, enabled: true })
|
|
})
|
|
|
|
test('planPermissionChanges: liest ueber 100 Dokumente seitenweise, offen-lassen und unlesbare Listen', async () => {
|
|
const api = fakeAdminApi()
|
|
const many = Array.from({ length: 230 }, (_, i) => ({ $id: `d${String(i).padStart(3, '0')}`, $permissions: ['read("users")'] }))
|
|
api.documents.splice(0, api.documents.length, ...many)
|
|
const all = await planPermissionChanges(api.request)
|
|
assert.equal(all.changes.filter((c) => c.kind === 'document').length, 230)
|
|
|
|
const kept = await planPermissionChanges(api.request, { keep: ['notes', 'legal-files'] })
|
|
assert.deepEqual(kept.changes.map((c) => `${c.kind}:${c.id}`), ['collection:workorders', 'bucket:broken'])
|
|
|
|
const failing = async (path, options) => {
|
|
if (path === '/storage/buckets') throw new Error('missing scope (buckets.read)')
|
|
return api.request(path, options)
|
|
}
|
|
const partial = await planPermissionChanges(failing)
|
|
assert.deepEqual(partial.errors, [{ path: '/storage/buckets', error: 'missing scope (buckets.read)' }])
|
|
assert.ok(partial.changes.some((c) => c.id === 'workorders'))
|
|
|
|
// --ohne-buckets: Buckets gar nicht erst lesen -> kein Fehler, nur Collections und Dokumente
|
|
const noBuckets = await planPermissionChanges(failing, { buckets: false })
|
|
assert.deepEqual(noBuckets.errors, [])
|
|
assert.deepEqual(noBuckets.changes.map((c) => c.kind).filter((k) => k === 'bucket'), [])
|
|
})
|
|
|
|
test('applyPermissionChanges: schreibt mit allen Einstellungen, meldet Fehler je Stelle, Ruecknahme schreibt before', async () => {
|
|
const api = fakeAdminApi()
|
|
const { changes } = await planPermissionChanges(api.request)
|
|
const results = await applyPermissionChanges(changes, { request: api.request })
|
|
assert.deepEqual(results.map((r) => r.ok), [true, true, true, false])
|
|
assert.match(results[3].error, /buckets\.write/)
|
|
|
|
const [collection, document, bucket] = api.writes
|
|
assert.deepEqual(collection, {
|
|
method: 'PUT',
|
|
path: '/databases/woms-database/collections/workorders',
|
|
body: { name: 'Work Orders', documentSecurity: false, enabled: true, permissions: changes[0].after },
|
|
})
|
|
assert.deepEqual(document, { method: 'PATCH', path: '/databases/woms-database/collections/notes/documents/n2', body: { permissions: changes[1].after } })
|
|
assert.equal(bucket.path, '/storage/buckets/legal-files')
|
|
assert.equal(bucket.body.maximumFileSize, 30000000)
|
|
assert.deepEqual(bucket.body.allowedFileExtensions, ['pdf'])
|
|
assert.equal(bucket.body.antivirus, true)
|
|
|
|
api.writes.length = 0
|
|
await applyPermissionChanges(changes.slice(0, 1), { direction: 'before', request: api.request })
|
|
assert.deepEqual(api.writes[0].body.permissions, ['read("users")', 'update("users")'])
|
|
})
|
|
|
|
test('Status: markiert Eintraege von Kunden und Eintraege ohne Label, listet Label-Konten ohne Eintrag', async () => {
|
|
aw.addUser({ $id: 'u_kenso', email: 'kenso@webklar.com', name: 'Kenso', labels: ['admin', 'mitarbeiter'] })
|
|
aw.addUser({ $id: 'u_andrej', email: 'andrej@webklar.com', name: 'Andrej' })
|
|
aw.addUser({ $id: 'u_kunde', email: 'kunde@example.invalid', name: 'Kunde' })
|
|
aw.addUser({ $id: 'u_nur_label', email: 'nico@webklar.com', name: 'Nico', labels: ['mitarbeiter'] })
|
|
aw.seed('employees', { $id: 'e_kenso', userId: 'u_kenso', displayName: 'Kenso', email: 'kenso@webklar.com' })
|
|
aw.seed('employees', { $id: 'e_andrej', userId: 'u_andrej', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
|
aw.seed('employees', { $id: 'e_kunde', userId: 'u_kunde', displayName: 'Kunde', email: 'kunde@example.invalid' })
|
|
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u_kunde', name: 'Kunde GmbH' })
|
|
|
|
const users = [...['u_kenso', 'u_andrej', 'u_kunde', 'u_nur_label']].map((id) => aw.user(id))
|
|
const request = async (path, options) => (path === '/users' ? { users } : (await import('../server/services/appwriteAdmin.js')).appwriteAdminRequest(path, options))
|
|
const { rows, labelOnly } = await staffAccessStatus(request)
|
|
const byId = Object.fromEntries(rows.map((r) => [r.employeeId, r]))
|
|
assert.equal(byId.e_kenso.staff, true)
|
|
assert.equal(byId.e_andrej.staff, false)
|
|
assert.equal(byId.e_andrej.customer, '')
|
|
assert.equal(byId.e_kunde.staff, false)
|
|
assert.equal(byId.e_kunde.customer, 'Kunde GmbH')
|
|
assert.deepEqual(labelOnly.map((u) => u.userId), ['u_nur_label'])
|
|
})
|
|
|
|
test('grantStaffLabel: haengt mitarbeiter an, vorhandene Labels bleiben', async () => {
|
|
aw.addUser({ $id: 'u_justin', email: 'justin@webklar.com', name: 'Justin', labels: ['vip'] })
|
|
const r = await grantStaffLabel('u_justin')
|
|
assert.deepEqual(r.labels, ['vip', 'mitarbeiter'])
|
|
assert.deepEqual(aw.user('u_justin').labels, ['vip', 'mitarbeiter'])
|
|
})
|
|
|
|
test('removeEmployeeEntry: loescht nur den Eintrag eines Kunden, nie das Konto; Mitarbeiter bleiben', async () => {
|
|
aw.addUser({ $id: 'u_k2', email: 'k2@example.invalid', name: 'K2' })
|
|
aw.addUser({ $id: 'u_m2', email: 'm2@webklar.com', name: 'M2', labels: ['mitarbeiter'] })
|
|
aw.seed('employees', { $id: 'e_k2', userId: 'u_k2', displayName: 'K2' })
|
|
aw.seed('employees', { $id: 'e_m2', userId: 'u_m2', displayName: 'M2' })
|
|
|
|
await removeEmployeeEntry('e_k2')
|
|
assert.equal(aw.docs('employees').has('e_k2'), false)
|
|
assert.ok(aw.user('u_k2'), 'Konto des Kunden bleibt')
|
|
|
|
await assert.rejects(removeEmployeeEntry('e_m2'), /Mitarbeiter-Label/)
|
|
assert.equal(aw.docs('employees').has('e_m2'), true)
|
|
})
|
|
|
|
test('removeEmployeeEntry: Konto-Abruf gestoert -> Abbruch, Eintrag bleibt (fail closed)', async () => {
|
|
aw.seed('employees', { $id: 'e_m3', userId: 'u_m2', displayName: 'M2' })
|
|
aw.failUserLookups(true)
|
|
try {
|
|
await assert.rejects(removeEmployeeEntry('e_m3'))
|
|
} finally {
|
|
aw.failUserLookups(false)
|
|
}
|
|
assert.equal(aw.docs('employees').has('e_m3'), true)
|
|
})
|
|
|
|
test('Mitarbeiter loeschen (Admin-Panel): Eintrag eines Kunden weg, sein Konto bleibt; Mitarbeiter-Konto wird geloescht', async () => {
|
|
aw.addUser({ $id: 'u_k3', email: 'k3@example.invalid', name: 'K3' })
|
|
aw.seed('employees', { $id: 'e_k3', userId: 'u_k3', displayName: 'K3' })
|
|
const res = await deleteEmployeeWithLogin('e_k3', 'u_kenso')
|
|
assert.equal(res.accountKept, true)
|
|
assert.equal(aw.docs('employees').has('e_k3'), false)
|
|
assert.ok(aw.user('u_k3'))
|
|
assert.ok(!aw.userCalls().includes('delete:u_k3'))
|
|
|
|
aw.addUser({ $id: 'u_m4', email: 'm4@webklar.com', name: 'M4', labels: ['mitarbeiter'] })
|
|
aw.seed('employees', { $id: 'e_m4', userId: 'u_m4', displayName: 'M4' })
|
|
await deleteEmployeeWithLogin('e_m4', 'u_kenso')
|
|
assert.equal(aw.user('u_m4'), null)
|
|
assert.ok(aw.userCalls().includes('delete:u_m4'))
|
|
})
|
|
|
|
test('Mitarbeiter anlegen: Label mitarbeiter (+ admin), Bearbeiten ergaenzt es und laesst admin ohne Angabe stehen', async () => {
|
|
const { employee } = await createEmployeeWithLogin({ displayName: 'Neu', email: 'neu@webklar.com', password: 'neu-neu-neu', isAdmin: false })
|
|
assert.deepEqual(aw.user(employee.userId).labels, ['mitarbeiter'])
|
|
|
|
const { employee: chef } = await createEmployeeWithLogin({ displayName: 'Chef', email: 'chef@webklar.com', password: 'chef-chef-chef', isAdmin: true })
|
|
assert.deepEqual(aw.user(chef.userId).labels.sort(), ['admin', 'mitarbeiter'])
|
|
assert.equal(chef.isAdmin, true)
|
|
|
|
// Bearbeiten gibt nie automatisch Zugang (der Eintrag kann von einem Kunden stammen); admin nur auf Angabe
|
|
aw.addUser({ $id: 'u_alt', email: 'alt@webklar.com', name: 'Alt', labels: [] })
|
|
aw.seed('employees', { $id: 'e_alt', userId: 'u_alt', displayName: 'Alt', email: 'alt@webklar.com' })
|
|
await updateEmployeeWithLogin('e_alt', { shortcode: 'ALT' }, 'u_kenso')
|
|
assert.deepEqual(aw.user('u_alt').labels, [])
|
|
await updateEmployeeWithLogin('e_alt', { isAdmin: true }, 'u_kenso')
|
|
assert.deepEqual(aw.user('u_alt').labels, ['admin'])
|
|
await updateEmployeeWithLogin('e_alt', { isAdmin: false }, 'u_kenso')
|
|
assert.deepEqual(aw.user('u_alt').labels, [])
|
|
})
|