test(mvp0): complete remaining i18n, RBAC, and CRUD coverage

This commit is contained in:
2026-02-11 12:06:27 +01:00
parent 8390689c8d
commit 3b130568e9
8 changed files with 238 additions and 9 deletions

View File

@@ -28,4 +28,31 @@ describe("rbac model", () => {
expect(permissionMatrix.editor.length).toBeGreaterThan(0)
expect(permissionMatrix.manager.length).toBeGreaterThan(0)
})
it("prevents privilege escalation for non-admin roles", () => {
expect(hasPermission("editor", "users:manage_roles", "global")).toBe(false)
expect(hasPermission("manager", "users:manage_roles", "global")).toBe(false)
expect(hasPermission("editor", "dashboard:read", "global")).toBe(true)
})
it("keeps role policy regressions visible for critical permissions", () => {
const criticalChecks: Array<{
role: "owner" | "support" | "admin" | "manager" | "editor"
permission: Parameters<typeof hasPermission>[1]
scope: Parameters<typeof hasPermission>[2]
allowed: boolean
}> = [
{ role: "owner", permission: "users:manage_roles", scope: "global", allowed: true },
{ role: "support", permission: "users:manage_roles", scope: "global", allowed: true },
{ role: "admin", permission: "banner:write", scope: "global", allowed: true },
{ role: "manager", permission: "users:write", scope: "global", allowed: false },
{ role: "manager", permission: "users:write", scope: "team", allowed: true },
{ role: "editor", permission: "news:publish", scope: "team", allowed: false },
{ role: "editor", permission: "news:publish", scope: "own", allowed: true },
]
for (const check of criticalChecks) {
expect(hasPermission(check.role, check.permission, check.scope)).toBe(check.allowed)
}
})
})

View File

@@ -0,0 +1,124 @@
import { describe, expect, it } from "vitest"
import { z } from "zod"
import { createCrudService } from "./service"
type RecordItem = {
id: string
title: string
}
describe("crud service contract", () => {
it("calls repository in expected order for update and delete", async () => {
const calls: string[] = []
const state = new Map<string, RecordItem>([["1", { id: "1", title: "Initial" }]])
const service = createCrudService({
resource: "item",
repository: {
list: async () => {
calls.push("list")
return Array.from(state.values())
},
findById: async (id) => {
calls.push(`findById:${id}`)
return state.get(id) ?? null
},
create: async (input: { title: string }) => {
calls.push("create")
return {
id: "2",
title: input.title,
}
},
update: async (id, input: { title?: string }) => {
calls.push(`update:${id}`)
const current = state.get(id)
if (!current) {
throw new Error("missing")
}
const updated = {
...current,
...input,
}
state.set(id, updated)
return updated
},
delete: async (id) => {
calls.push(`delete:${id}`)
const current = state.get(id)
if (!current) {
throw new Error("missing")
}
state.delete(id)
return current
},
},
schemas: {
create: z.object({
title: z.string().min(3),
}),
update: z.object({
title: z.string().min(3).optional(),
}),
},
})
await service.update("1", { title: "Updated" })
await service.delete("1")
expect(calls).toEqual(["findById:1", "update:1", "findById:1", "delete:1"])
})
it("passes parsed payload to repository create/update contracts", async () => {
let createPayload: unknown = null
let updatePayload: unknown = null
const service = createCrudService({
resource: "item",
repository: {
list: async () => [],
findById: async () => ({
id: "1",
title: "Existing",
}),
create: async (input: { title: string }) => {
createPayload = input
return {
id: "2",
title: input.title,
}
},
update: async (_id, input: { title?: string }) => {
updatePayload = input
return {
id: "1",
title: input.title ?? "Existing",
}
},
delete: async () => ({
id: "1",
title: "Existing",
}),
},
schemas: {
create: z.object({
title: z.string().trim().min(3),
}),
update: z.object({
title: z.string().trim().min(3).optional(),
}),
},
})
await service.create({
title: " Created ",
})
await service.update("1", {
title: " Updated ",
})
expect(createPayload).toEqual({ title: "Created" })
expect(updatePayload).toEqual({ title: "Updated" })
})
})