From f8612cfcb8e252ea748d6ae58c4c790afe775486 Mon Sep 17 00:00:00 2001 From: Yunxiao Xu Date: Wed, 11 Feb 2026 20:30:32 -0800 Subject: [PATCH] fix(auth): Correct API paths and resolve build/type errors in tests and config --- frontend/src/lib/validations/auth.ts | 18 +++++++++++ frontend/src/services/auth.test.ts | 20 +++++++++--- frontend/src/services/auth.ts | 47 ++++++++++++++++++++++++++++ frontend/src/test/setup.ts | 27 ++++++++++++++++ frontend/vite.config.ts | 3 +- 5 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 frontend/src/lib/validations/auth.ts create mode 100644 frontend/src/services/auth.ts diff --git a/frontend/src/lib/validations/auth.ts b/frontend/src/lib/validations/auth.ts new file mode 100644 index 0000000..d86f301 --- /dev/null +++ b/frontend/src/lib/validations/auth.ts @@ -0,0 +1,18 @@ +import { z } from "zod" + +export const loginSchema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), +}) + +export const registerSchema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), + confirmPassword: z.string().min(6, "Password must be at least 6 characters"), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords do not match", + path: ["confirmPassword"], +}) + +export type LoginInput = z.infer +export type RegisterInput = z.infer diff --git a/frontend/src/services/auth.test.ts b/frontend/src/services/auth.test.ts index bb459d3..ac32444 100644 --- a/frontend/src/services/auth.test.ts +++ b/frontend/src/services/auth.test.ts @@ -1,9 +1,11 @@ -import { describe, it, expect, vi, beforeEach } from "vitest" +import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest" import axios from "axios" import { AuthService } from "./auth" vi.mock("axios") -const mockedAxios = axios as jest.Mocked +const mockedAxios = axios as unknown as { + post: MockInstance +} describe("AuthService", () => { beforeEach(() => { @@ -22,9 +24,16 @@ describe("AuthService", () => { const result = await AuthService.login("test@example.com", "password123") - expect(mockedAxios.post).toHaveBeenCalledWith("/api/auth/login", expect.any(FormData)) + expect(mockedAxios.post).toHaveBeenCalledWith("/auth/login", expect.any(FormData)) + + // Validate FormData content + const formData = (mockedAxios.post.mock.calls[0][1] as FormData) + expect(formData.get("username")).toBe("test@example.com") + expect(formData.get("password")).toBe("password123") + expect(result.access_token).toBe("fake-jwt-token") expect(localStorage.getItem("token")).toBe("fake-jwt-token") + expect(AuthService.isAuthenticated()).toBe(true) }) it("handles login failure", async () => { @@ -32,6 +41,7 @@ describe("AuthService", () => { await expect(AuthService.login("test@example.com", "wrong")).rejects.toThrow("Invalid credentials") expect(localStorage.getItem("token")).toBeNull() + expect(AuthService.isAuthenticated()).toBe(false) }) it("successfully registers a user", async () => { @@ -45,7 +55,7 @@ describe("AuthService", () => { const result = await AuthService.register("test@example.com", "password123") - expect(mockedAxios.post).toHaveBeenCalledWith("/api/auth/register", { + expect(mockedAxios.post).toHaveBeenCalledWith("/auth/register", { email: "test@example.com", password: "password123", }) @@ -54,7 +64,9 @@ describe("AuthService", () => { it("logs out and clears the token", () => { localStorage.setItem("token", "some-token") + expect(AuthService.isAuthenticated()).toBe(true) AuthService.logout() expect(localStorage.getItem("token")).toBeNull() + expect(AuthService.isAuthenticated()).toBe(false) }) }) diff --git a/frontend/src/services/auth.ts b/frontend/src/services/auth.ts new file mode 100644 index 0000000..7d510d5 --- /dev/null +++ b/frontend/src/services/auth.ts @@ -0,0 +1,47 @@ +import axios from "axios" + +const API_URL = import.meta.env.VITE_API_URL || "" + +export interface AuthResponse { + access_token: string + token_type: string +} + +export interface UserResponse { + id: string + email: string +} + +export const AuthService = { + async login(email: string, password: string): Promise { + const formData = new FormData() + formData.append("username", email) // OAuth2PasswordRequestForm uses 'username' + formData.append("password", password) + + const response = await axios.post(`${API_URL}/auth/login`, formData) + if (response.data.access_token) { + localStorage.setItem("token", response.data.access_token) + } + return response.data + }, + + async register(email: string, password: string): Promise { + const response = await axios.post(`${API_URL}/auth/register`, { + email, + password, + }) + return response.data + }, + + logout() { + localStorage.removeItem("token") + }, + + getToken() { + return localStorage.getItem("token") + }, + + isAuthenticated() { + return !!this.getToken() + }, +} diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts index dcb28f4..ed0cc11 100644 --- a/frontend/src/test/setup.ts +++ b/frontend/src/test/setup.ts @@ -17,6 +17,33 @@ Object.defineProperty(window, "matchMedia", { })), }) +// More robust localStorage mock +const localStorageMock = (() => { + let store: Record = {} + return { + getItem: vi.fn((key: string) => (Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null)), + setItem: vi.fn((key: string, value: string) => { + store[key] = value.toString() + }), + clear: vi.fn(() => { + store = {} + }), + removeItem: vi.fn((key: string) => { + delete store[key] + }), + get length() { + return Object.keys(store).length + }, + key: vi.fn((index: number) => Object.keys(store)[index] || null), + } +})() + +Object.defineProperty(window, "localStorage", { + value: localStorageMock, + writable: true +}) + afterEach(() => { cleanup() + vi.clearAllMocks() }) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b19e9e4..facde91 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,8 +1,7 @@ -/// import path from "path" import tailwindcss from "@tailwindcss/vite" import react from "@vitejs/plugin-react" -import { defineConfig } from "vite" +import { defineConfig } from "vitest/config" // https://vite.dev/config/ export default defineConfig({