test(tooling): add vitest baseline policy and order tests
This commit is contained in:
+7
-4
@@ -11,7 +11,9 @@
|
|||||||
"format": "prettier \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,yml,yaml}\" --write",
|
"format": "prettier \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,yml,yaml}\" --write",
|
||||||
"format:check": "prettier \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,yml,yaml}\" --check",
|
"format:check": "prettier \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,yml,yaml}\" --check",
|
||||||
"check": "pnpm lint && pnpm typecheck && pnpm format:check",
|
"check": "pnpm lint && pnpm typecheck && pnpm format:check",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
@@ -33,11 +35,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
||||||
"@typescript-eslint/parser": "^8.56.0",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||||
|
"@typescript-eslint/parser": "^8.56.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-config-next": "^16.1.6",
|
"eslint-config-next": "^16.1.6",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
"shadcn": "^3.8.5",
|
"shadcn": "^3.8.5",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+798
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import { evaluateOrderTransition } from "@/lib/domain/order-machine"
|
||||||
|
import type { Actor } from "@/lib/policy/actor"
|
||||||
|
import type { UserRole } from "@/lib/types"
|
||||||
|
|
||||||
|
function actor(role: UserRole, userId = "u1"): Actor {
|
||||||
|
return { role, userId }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("evaluateOrderTransition", () => {
|
||||||
|
it("allows valid transition for pay", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
actor: actor("consumer"),
|
||||||
|
order: { status: "pending_payment" },
|
||||||
|
action: "PAY",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision.ok).toBe(true)
|
||||||
|
expect(result.nextStatus).toBe("pending_accept")
|
||||||
|
expect(result.sideEffects).toContainEqual({
|
||||||
|
type: "SCHEDULE_TIMEOUT",
|
||||||
|
status: "pending_accept",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("denies invalid status transition", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
actor: actor("consumer"),
|
||||||
|
order: { status: "completed" },
|
||||||
|
action: "PAY",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
reasonCode: "INVALID_STATUS",
|
||||||
|
})
|
||||||
|
expect(result.nextStatus).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("denies role forbidden actions", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
actor: actor("consumer"),
|
||||||
|
order: { status: "pending_accept" },
|
||||||
|
action: "ACCEPT",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
reasonCode: "ROLE_FORBIDDEN",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows accept for player", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
actor: actor("player"),
|
||||||
|
order: { status: "pending_accept" },
|
||||||
|
action: "ACCEPT",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision.ok).toBe(true)
|
||||||
|
expect(result.nextStatus).toBe("in_progress")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows close confirmation to pending_review", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
actor: actor("consumer"),
|
||||||
|
order: { status: "pending_close" },
|
||||||
|
action: "CONFIRM_CLOSE",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision.ok).toBe(true)
|
||||||
|
expect(result.nextStatus).toBe("pending_review")
|
||||||
|
expect(result.sideEffects).toContainEqual({
|
||||||
|
type: "SCHEDULE_TIMEOUT",
|
||||||
|
status: "pending_review",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows auto timeout actions without actor", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
order: { status: "pending_close" },
|
||||||
|
action: "AUTO_TIMEOUT_PENDING_CLOSE",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision.ok).toBe(true)
|
||||||
|
expect(result.nextStatus).toBe("pending_review")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("adds payout side effect when entering completed", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
order: { status: "pending_review" },
|
||||||
|
action: "AUTO_TIMEOUT_PENDING_REVIEW",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision.ok).toBe(true)
|
||||||
|
expect(result.nextStatus).toBe("completed")
|
||||||
|
expect(result.sideEffects).toContainEqual({ type: "PAYOUT_INCOME" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("supports resolving dispute by owner", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
actor: actor("owner"),
|
||||||
|
order: { status: "disputed" },
|
||||||
|
action: "RESOLVE_DISPUTE",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision.ok).toBe(true)
|
||||||
|
expect(result.nextStatus).toBe("pending_review")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects dispute resolution by non-owner", () => {
|
||||||
|
const result = evaluateOrderTransition({
|
||||||
|
actor: actor("player"),
|
||||||
|
order: { status: "disputed" },
|
||||||
|
action: "RESOLVE_DISPUTE",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.decision).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
reasonCode: "ROLE_FORBIDDEN",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import { allow, deny, requireAuth } from "@/lib/policy/assert"
|
||||||
|
|
||||||
|
describe("policy decision helpers", () => {
|
||||||
|
it("returns ok for allow", () => {
|
||||||
|
expect(allow()).toEqual({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns reason code for deny", () => {
|
||||||
|
expect(deny("ROLE_FORBIDDEN", "forbidden")).toEqual({
|
||||||
|
ok: false,
|
||||||
|
reasonCode: "ROLE_FORBIDDEN",
|
||||||
|
message: "forbidden",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("requires auth actor", () => {
|
||||||
|
expect(requireAuth(undefined)).toEqual({
|
||||||
|
ok: false,
|
||||||
|
reasonCode: "AUTH_REQUIRED",
|
||||||
|
message: "请先登录",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
requireAuth({
|
||||||
|
userId: "u1",
|
||||||
|
role: "consumer",
|
||||||
|
}),
|
||||||
|
).toEqual({ ok: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { dirname, resolve } from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
|
const rootDir = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "node",
|
||||||
|
include: ["tests/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": resolve(rootDir, "."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user