From 20f159abf06599bc8902a445be21d4c085d82ede Mon Sep 17 00:00:00 2001 From: nsfisis Date: Wed, 3 Dec 2025 06:00:48 +0900 Subject: test(auth): add tests for refresh token endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive tests for POST /refresh endpoint covering: - Valid refresh token returns new access/refresh tokens - Invalid refresh token returns 401 - Expired refresh token returns 401 - User not found returns 401 - Missing/empty refresh token returns 422 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pkgs/server/src/routes/auth.test.ts | 205 ++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) (limited to 'pkgs/server') diff --git a/pkgs/server/src/routes/auth.test.ts b/pkgs/server/src/routes/auth.test.ts index 28bd558..7de04f5 100644 --- a/pkgs/server/src/routes/auth.test.ts +++ b/pkgs/server/src/routes/auth.test.ts @@ -37,6 +37,9 @@ vi.mock("../db", () => { }), })), })), + delete: vi.fn(() => ({ + where: vi.fn(() => Promise.resolve(undefined)), + })), }, users: { id: "id", @@ -306,3 +309,205 @@ describe("POST /login", () => { expect(body.error?.code).toBe("VALIDATION_ERROR"); }); }); + +interface RefreshResponse { + accessToken?: string; + refreshToken?: string; + user?: { + id: string; + username: string; + }; + error?: { + code: string; + message: string; + }; +} + +describe("POST /refresh", () => { + let app: Hono; + + beforeEach(() => { + vi.clearAllMocks(); + app = new Hono(); + app.onError(errorHandler); + app.route("/api/auth", auth); + }); + + it("returns new tokens for valid refresh token", async () => { + const { db } = await import("../db"); + + // Mock finding valid refresh token + vi.mocked(db.select).mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([ + { + id: "token-id-123", + userId: "user-uuid-123", + expiresAt: new Date(Date.now() + 86400000), // expires in 1 day + }, + ]), + }), + }), + } as unknown as ReturnType); + + // Mock finding user + vi.mocked(db.select).mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([ + { + id: "user-uuid-123", + username: "testuser", + }, + ]), + }), + }), + } as unknown as ReturnType); + + // Mock delete old token + vi.mocked(db.delete).mockReturnValueOnce({ + where: vi.fn().mockResolvedValue(undefined), + } as unknown as ReturnType); + + // Mock insert new token + vi.mocked(db.insert).mockReturnValueOnce({ + values: vi.fn().mockResolvedValue(undefined), + } as unknown as ReturnType); + + const res = await app.request("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + refreshToken: "valid-refresh-token-hex", + }), + }); + + expect(res.status).toBe(200); + const body = (await res.json()) as RefreshResponse; + expect(body.accessToken).toBeDefined(); + expect(typeof body.accessToken).toBe("string"); + expect(body.refreshToken).toBeDefined(); + expect(typeof body.refreshToken).toBe("string"); + expect(body.user).toEqual({ + id: "user-uuid-123", + username: "testuser", + }); + }); + + it("returns 401 for invalid refresh token", async () => { + const { db } = await import("../db"); + + // Mock no token found + vi.mocked(db.select).mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }), + } as unknown as ReturnType); + + const res = await app.request("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + refreshToken: "invalid-refresh-token", + }), + }); + + expect(res.status).toBe(401); + const body = (await res.json()) as RefreshResponse; + expect(body.error?.code).toBe("INVALID_REFRESH_TOKEN"); + }); + + it("returns 401 for expired refresh token", async () => { + const { db } = await import("../db"); + + // Mock no valid (non-expired) token found (empty result because expiry check in query) + vi.mocked(db.select).mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }), + } as unknown as ReturnType); + + const res = await app.request("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + refreshToken: "expired-refresh-token", + }), + }); + + expect(res.status).toBe(401); + const body = (await res.json()) as RefreshResponse; + expect(body.error?.code).toBe("INVALID_REFRESH_TOKEN"); + }); + + it("returns 401 when user not found", async () => { + const { db } = await import("../db"); + + // Mock finding valid refresh token + vi.mocked(db.select).mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([ + { + id: "token-id-123", + userId: "deleted-user-id", + expiresAt: new Date(Date.now() + 86400000), + }, + ]), + }), + }), + } as unknown as ReturnType); + + // Mock user not found + vi.mocked(db.select).mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }), + } as unknown as ReturnType); + + const res = await app.request("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + refreshToken: "valid-refresh-token", + }), + }); + + expect(res.status).toBe(401); + const body = (await res.json()) as RefreshResponse; + expect(body.error?.code).toBe("USER_NOT_FOUND"); + }); + + it("returns 422 for missing refresh token", async () => { + const res = await app.request("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + + expect(res.status).toBe(422); + const body = (await res.json()) as RefreshResponse; + expect(body.error?.code).toBe("VALIDATION_ERROR"); + }); + + it("returns 422 for empty refresh token", async () => { + const res = await app.request("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + refreshToken: "", + }), + }); + + expect(res.status).toBe(422); + const body = (await res.json()) as RefreshResponse; + expect(body.error?.code).toBe("VALIDATION_ERROR"); + }); +}); -- cgit v1.2.3-70-g09d2