refactor: convert to bun workspaces monorepo

- Move web app into apps/web/
- Create packages/shared/ with shared types
- Create plugins/decky-vault/ scaffold
- Root package.json manages workspaces only
This commit is contained in:
2026-06-28 05:20:28 +08:00
parent c4bede20d4
commit cd72b7a948
345 changed files with 488 additions and 126 deletions
+137
View File
@@ -0,0 +1,137 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { db } from "@/lib/db/index"
import { checkAndAutoPin } from "@/lib/api/auto-pin"
// Mock drizzle-orm to avoid ESM named-export resolution errors between test files
vi.mock("drizzle-orm", () => ({
eq: vi.fn((col, val) => ({ col, val })),
}))
vi.mock("drizzle-orm/pg-core", () => ({
pgTable: vi.fn((name, columns, indexes) => ({ name, columns, indexes })),
pgEnum: vi.fn((name, values) => ({ name, values })),
text: vi.fn((name) => name),
integer: vi.fn((name) => name),
real: vi.fn((name) => name),
boolean: vi.fn((name) => name),
timestamp: vi.fn((name) => name),
jsonb: vi.fn((name) => name),
index: vi.fn((name) => ({ on: vi.fn() })),
}))
// Mock db schema — only the symbols the module under test references
vi.mock("@/lib/db/schema", () => ({
performanceEntries: {
id: "id",
isPinned: "is_pinned",
isRemoved: "is_removed",
upvotes: "upvotes",
downvotes: "downvotes",
},
}))
// Mock the database
vi.mock("@/lib/db/index", () => ({
db: {
select: vi.fn(),
update: vi.fn(),
},
}))
function setupMockEntry(overrides: {
id?: string
isPinned?: boolean
isRemoved?: boolean
upvotes?: number
downvotes?: number
}) {
const entry = {
id: overrides.id ?? "test-entry-1",
isPinned: overrides.isPinned ?? false,
isRemoved: overrides.isRemoved ?? false,
upvotes: overrides.upvotes ?? 0,
downvotes: overrides.downvotes ?? 0,
}
// Mock select chain: .select(...).from(...).where(...).limit(...)
const limitMock = vi.fn().mockResolvedValue([entry])
const whereMock = vi.fn().mockReturnValue({ limit: limitMock })
const fromMock = vi.fn().mockReturnValue({ where: whereMock })
const selectMock = vi.fn().mockReturnValue({ from: fromMock })
;(db.select as any).mockReturnValue({ from: fromMock })
// Mock update chain: .update(...).set(...).where(...)
const updateWhereMock = vi.fn().mockResolvedValue(undefined)
const updateSetMock = vi.fn().mockReturnValue({ where: updateWhereMock })
const updateMock = vi.fn().mockReturnValue({ set: updateSetMock })
;(db.update as any).mockImplementation(updateMock)
return entry
}
describe("checkAndAutoPin", () => {
beforeEach(() => {
vi.clearAllMocks()
})
it("pins an entry with 8 upvotes and 2 downvotes (80% approval, 10 total)", async () => {
setupMockEntry({ upvotes: 8, downvotes: 2 })
const result = await checkAndAutoPin("test-entry-1")
expect(result).toBe(true)
})
it("does NOT pin an entry with 7 upvotes and 1 downvote (< 8 absolute upvotes)", async () => {
setupMockEntry({ upvotes: 7, downvotes: 1 })
const result = await checkAndAutoPin("test-entry-1")
expect(result).toBe(false)
})
it("does NOT pin an entry with 8 upvotes and 0 downvotes (< 10 total votes)", async () => {
setupMockEntry({ upvotes: 8, downvotes: 0 })
const result = await checkAndAutoPin("test-entry-1")
expect(result).toBe(false)
})
it("does NOT pin an entry with 6 upvotes and 4 downvotes (60% approval, below 80%)", async () => {
setupMockEntry({ upvotes: 6, downvotes: 4 })
const result = await checkAndAutoPin("test-entry-1")
expect(result).toBe(false)
})
it("does NOT pin an already-pinned entry", async () => {
setupMockEntry({ isPinned: true, upvotes: 20, downvotes: 0 })
const result = await checkAndAutoPin("test-entry-1")
expect(result).toBe(false)
})
it("does NOT pin a removed entry", async () => {
setupMockEntry({ isRemoved: true, upvotes: 20, downvotes: 0 })
const result = await checkAndAutoPin("test-entry-1")
expect(result).toBe(false)
})
it("pins an entry with 20 upvotes and 5 downvotes (80% approval exactly)", async () => {
setupMockEntry({ upvotes: 20, downvotes: 5 })
const result = await checkAndAutoPin("test-entry-1")
expect(result).toBe(true)
})
it("does NOT pin when entry is not found", async () => {
// Mock select returning empty array
const limitMock = vi.fn().mockResolvedValue([])
const whereMock = vi.fn().mockReturnValue({ limit: limitMock })
const fromMock = vi.fn().mockReturnValue({ where: whereMock })
const selectMock = vi.fn().mockReturnValue({ from: fromMock })
;(db.select as any).mockReturnValue({ from: fromMock })
;(db.update as any) = vi.fn()
const result = await checkAndAutoPin("nonexistent")
expect(result).toBe(false)
})
})
@@ -0,0 +1,64 @@
import { describe, it, expect } from "vitest"
import { loginEmailSchema } from "@/lib/auth/validation"
describe("check-email validation", () => {
it("accepts valid email", () => {
const result = loginEmailSchema.safeParse({ email: "user@example.com" })
expect(result.success).toBe(true)
})
it("rejects missing email", () => {
const result = loginEmailSchema.safeParse({})
expect(result.success).toBe(false)
})
it("rejects invalid email format", () => {
const result = loginEmailSchema.safeParse({ email: "not-email" })
expect(result.success).toBe(false)
})
it("rejects empty string email", () => {
const result = loginEmailSchema.safeParse({ email: "" })
expect(result.success).toBe(false)
})
})
describe("check-email response contract", () => {
it("returns { exists: boolean } on success", () => {
const successShape = { exists: true }
const failureShape = { exists: false }
expect(successShape).toHaveProperty("exists")
expect(failureShape).toHaveProperty("exists")
expect(typeof successShape.exists).toBe("boolean")
expect(typeof failureShape.exists).toBe("boolean")
})
it("returns { error: string } on validation failure", () => {
const errorShape = { error: "Invalid request body" }
expect(errorShape).toHaveProperty("error")
expect(typeof errorShape.error).toBe("string")
})
})
describe("check-email does NOT block deckyvault.xyz for login", () => {
it("accepts a @deckyvault.xyz email through loginEmailSchema", () => {
const result = loginEmailSchema.safeParse({
email: "admin@deckyvault.xyz",
})
expect(result.success).toBe(true)
})
it("accepts a @DECKYVAULT.XYZ email (uppercase) through loginEmailSchema", () => {
const result = loginEmailSchema.safeParse({
email: "admin@DECKYVAULT.XYZ",
})
expect(result.success).toBe(true)
})
it("the response contract still matches { exists: boolean }", () => {
const successShape = { exists: true }
const failureShape = { exists: false }
expect(typeof successShape.exists).toBe("boolean")
expect(typeof failureShape.exists).toBe("boolean")
})
})
@@ -0,0 +1,117 @@
import { describe, it, expect } from "vitest"
import { Elysia } from "elysia"
import { isDeckyVaultEmail, DOMAIN_BLOCK_ERROR } from "@/lib/auth/domain-block"
/**
* The domain-block onBeforeHandle handler mirrors the logic
* wired into the auth group in lib/api/app.ts. Tested in isolation
* here so we don't need to stand up the full app / DB.
*
* The handler is typed loosely to avoid Elysia's complex context type
* in test environments — the actual type safety is verified against
* the real app.ts implementation at integration test time.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const domainBlockOnBeforeHandle = async (context: any) => {
const { request, set } = context as {
request: Request
set: { status: number }
}
const url = new URL(request.url)
const isSignUp =
url.pathname === "/api/auth/sign-up/email" &&
request.method === "POST"
if (!isSignUp) return
if (process.env.NODE_ENV !== "development") {
try {
const cloned = request.clone()
const body = await cloned.json()
if (isDeckyVaultEmail(body.email)) {
set.status = 400
return { error: DOMAIN_BLOCK_ERROR }
}
} catch {
// Malformed body — let Better Auth reject it downstream
}
}
}
describe("domain block middleware", () => {
it("blocks POST /api/auth/sign-up/email with @deckyvault.xyz email", async () => {
const app = new Elysia()
.onBeforeHandle(domainBlockOnBeforeHandle)
.post("/api/auth/sign-up/email", () => ({
success: "should not reach this",
}))
const res = await app.handle(
new Request("http://localhost:3000/api/auth/sign-up/email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "bad@deckyvault.xyz",
password: "abcdefghij",
name: "Test",
}),
}),
)
expect(res.status).toBe(400)
const body = await res.json()
expect(body.error).toBe(DOMAIN_BLOCK_ERROR)
})
it("allows POST /api/auth/sign-up/email with non-deckyvault email", async () => {
const app = new Elysia()
.onBeforeHandle(domainBlockOnBeforeHandle)
.post("/api/auth/sign-up/email", () => ({
success: true,
}))
const res = await app.handle(
new Request("http://localhost:3000/api/auth/sign-up/email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "good@gmail.com",
password: "abcdefghij",
name: "Test",
}),
}),
)
expect(res.status).toBe(200)
})
it("ignores non-sign-up routes (GET /api/auth/something)", async () => {
const app = new Elysia()
.onBeforeHandle(domainBlockOnBeforeHandle)
.get("/api/auth/something", () => ({ ok: true }))
const res = await app.handle(
new Request("http://localhost:3000/api/auth/something"),
)
expect(res.status).toBe(200)
const body = await res.json()
expect(body.ok).toBe(true)
})
it("handles malformed JSON body gracefully (passes through)", async () => {
const app = new Elysia()
.onBeforeHandle(domainBlockOnBeforeHandle)
.post("/api/auth/sign-up/email", () => ({ success: true }))
const res = await app.handle(
new Request("http://localhost:3000/api/auth/sign-up/email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "not-valid-json",
}),
)
expect(res.status).toBe(200)
})
})
@@ -0,0 +1,23 @@
import { describe, it, expect } from "vitest"
describe("API version", () => {
it("matches package.json version", () => {
const pkg = require("../../../package.json")
const expectedVersion = pkg.version
expect(expectedVersion).toBe("2026.2.2")
})
})
describe("set-password endpoint security", () => {
it("only accepts JSON body for newPassword (not URL-encoded or form-data)", () => {
const allowedContentTypes = ["application/json"]
const forbiddenContentTypes = [
"application/x-www-form-urlencoded",
"multipart/form-data",
]
expect(allowedContentTypes).toContain("application/json")
for (const ct of forbiddenContentTypes) {
expect(allowedContentTypes).not.toContain(ct)
}
})
})
@@ -0,0 +1,59 @@
import { describe, it, expect } from "vitest"
import fs from "node:fs"
import path from "node:path"
describe("rate limiting removal", () => {
const cwd = process.cwd()
it("lib/auth/rate-limit.ts no longer exports the rateLimit function", () => {
const filePath = path.join(cwd, "lib", "auth", "rate-limit.ts")
expect(fs.existsSync(filePath)).toBe(false)
})
it("lib/api/app.ts no longer imports or uses rateLimit", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "api", "app.ts"),
"utf8",
)
expect(content).not.toContain('import { rateLimit }')
expect(content).not.toContain('.use(rateLimit(')
})
it("lib/auth.ts has Better Auth rateLimit disabled", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "auth.ts"),
"utf8",
)
const hasRateLimitBlock = content.includes("rateLimit:")
if (hasRateLimitBlock) {
expect(content).toContain("enabled: false")
}
})
it("lib/api/contact.ts no longer calls checkContactRateLimit", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "api", "contact.ts"),
"utf8",
)
expect(content).not.toContain("checkContactRateLimit")
})
it("lib/api/comments.ts no longer enforces hourly cap", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "api", "comments.ts"),
"utf8",
)
expect(content).not.toContain("hourly limit")
expect(content).not.toContain('"Duplicate comment detected')
})
it("lib/api/performance-submit.ts no longer enforces submission cooldown", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "api", "performance-submit.ts"),
"utf8",
)
expect(content).not.toContain("Submission cooldown")
expect(content).not.toContain("sixtySecondsAgo")
expect(content).not.toContain("Please wait before submitting")
})
})