fix(auth): Correct API paths and resolve build/type errors in tests and config

This commit is contained in:
Yunxiao Xu
2026-02-11 20:30:32 -08:00
parent e3bc699c64
commit f8612cfcb8
5 changed files with 109 additions and 6 deletions

View File

@@ -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<typeof loginSchema>
export type RegisterInput = z.infer<typeof registerSchema>

View File

@@ -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<typeof axios>
const mockedAxios = axios as unknown as {
post: MockInstance<typeof axios.post>
}
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)
})
})

View File

@@ -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<AuthResponse> {
const formData = new FormData()
formData.append("username", email) // OAuth2PasswordRequestForm uses 'username'
formData.append("password", password)
const response = await axios.post<AuthResponse>(`${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<UserResponse> {
const response = await axios.post<UserResponse>(`${API_URL}/auth/register`, {
email,
password,
})
return response.data
},
logout() {
localStorage.removeItem("token")
},
getToken() {
return localStorage.getItem("token")
},
isAuthenticated() {
return !!this.getToken()
},
}

View File

@@ -17,6 +17,33 @@ Object.defineProperty(window, "matchMedia", {
})),
})
// More robust localStorage mock
const localStorageMock = (() => {
let store: Record<string, string> = {}
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()
})