diff --git a/README.md b/README.md index e0678bd..57d85b0 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,11 @@ ## 开发与部署 -用 Linux 开发与部署,包管理器采用 bun@1.3.11。数据库为 **postgres**(通过 `DATABASE_URL` 连接;本地可参考 `.env.example` 复制为 `.env.local`)。部署时可直接打包 `.output` 目录,在服务器环境执行迁移命令,省时省力。 \ No newline at end of file +用 Linux 开发与部署,包管理器采用 bun@1.3.11。数据库为 **postgres**(通过 `DATABASE_URL` 连接;本地可参考 `.env.example` 复制为 `.env.local`)。部署时可直接打包 `.output` 目录,在服务器环境执行迁移命令,省时省力。 + +会话与限流依赖 **Redis**(`REDIS_URL`,见 `.env.example`)。认证相关设计见 `docs/superpowers/specs/2026-04-12-auth-user-design.md`,实现任务见 `docs/superpowers/plans/2026-04-12-auth-user.md`。 + +- 迁移:`bun run db:migrate`(需可用 `DATABASE_URL`)。 +- 生成迁移:在仓库根执行 `bun run db:generate -- --name `(`--` 之后参数传给 `drizzle-kit generate`)。 +- 单元测试:`bun run test`。 +- 可选 HTTP 集成:终端 A `bun run dev`(需 `DATABASE_URL`、`REDIS_URL`),终端 B `TEST_INTEGRATION=1 NODE_ENV=test bun run test:integration`。 \ No newline at end of file diff --git a/package.json b/package.json index cca62d5..3c00569 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "cp:db": "cp build-files/run.sh .output/run.sh && cp .env.example .output/.env && cp build-files/migrate.js .output/server/migrate.js", "migrate:test": "sh scripts/migrate-test.sh", "db:migrate": "bun --elide-lines=0 --filter drizzle-pkg migrate", - "db:generate": "bun --elide-lines=0 --filter drizzle-pkg generate --name", + "db:generate": "bun --elide-lines=0 --filter drizzle-pkg run generate --", "db:seed": "bun --elide-lines=0 --filter drizzle-pkg seed", "generate": "nuxt generate", "preview": "nuxt preview", diff --git a/test/integration/auth.http.test.ts b/test/integration/auth.http.test.ts new file mode 100644 index 0000000..fb5dea1 --- /dev/null +++ b/test/integration/auth.http.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "bun:test"; + +const BASE = process.env.TEST_BASE_URL ?? "http://127.0.0.1:3000"; + +function parseCookie(res: Response): string { + const raw = res.headers.get("set-cookie"); + if (!raw) return ""; + return raw.split(";")[0] ?? ""; +} + +(process.env.TEST_INTEGRATION ? describe : describe.skip)("auth over HTTP", () => { + it("register → me → verify → patch", async () => { + const email = `u${Date.now()}@example.com`; + const reg = await fetch(`${BASE}/api/auth/register`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + email, + password: "password123", + name: "T", + age: 30, + }), + }); + expect(reg.status).toBe(200); + const regJson = (await reg.json()) as { + user: { emailVerified: boolean }; + _testTokens?: { verify?: string }; + }; + expect(regJson.user.emailVerified).toBe(false); + const cookie = parseCookie(reg); + expect(cookie.length).toBeGreaterThan(5); + + const me1 = await fetch(`${BASE}/api/me`, { headers: { cookie } }); + expect(me1.status).toBe(200); + + const patch1 = await fetch(`${BASE}/api/me`, { + method: "PATCH", + headers: { cookie, "content-type": "application/json" }, + body: JSON.stringify({ name: "T2" }), + }); + expect(patch1.status).toBe(403); + + const verify = await fetch(`${BASE}/api/auth/verify-email`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ token: regJson._testTokens?.verify }), + }); + expect(verify.status).toBe(200); + + const patch2 = await fetch(`${BASE}/api/me`, { + method: "PATCH", + headers: { cookie, "content-type": "application/json" }, + body: JSON.stringify({ name: "T2" }), + }); + expect(patch2.status).toBe(200); + }); +});