fix(auth): Correct API paths and resolve build/type errors in tests and config
This commit is contained in:
18
frontend/src/lib/validations/auth.ts
Normal file
18
frontend/src/lib/validations/auth.ts
Normal 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>
|
||||||
@@ -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 axios from "axios"
|
||||||
import { AuthService } from "./auth"
|
import { AuthService } from "./auth"
|
||||||
|
|
||||||
vi.mock("axios")
|
vi.mock("axios")
|
||||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
const mockedAxios = axios as unknown as {
|
||||||
|
post: MockInstance<typeof axios.post>
|
||||||
|
}
|
||||||
|
|
||||||
describe("AuthService", () => {
|
describe("AuthService", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -22,9 +24,16 @@ describe("AuthService", () => {
|
|||||||
|
|
||||||
const result = await AuthService.login("test@example.com", "password123")
|
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(result.access_token).toBe("fake-jwt-token")
|
||||||
expect(localStorage.getItem("token")).toBe("fake-jwt-token")
|
expect(localStorage.getItem("token")).toBe("fake-jwt-token")
|
||||||
|
expect(AuthService.isAuthenticated()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles login failure", async () => {
|
it("handles login failure", async () => {
|
||||||
@@ -32,6 +41,7 @@ describe("AuthService", () => {
|
|||||||
|
|
||||||
await expect(AuthService.login("test@example.com", "wrong")).rejects.toThrow("Invalid credentials")
|
await expect(AuthService.login("test@example.com", "wrong")).rejects.toThrow("Invalid credentials")
|
||||||
expect(localStorage.getItem("token")).toBeNull()
|
expect(localStorage.getItem("token")).toBeNull()
|
||||||
|
expect(AuthService.isAuthenticated()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully registers a user", async () => {
|
it("successfully registers a user", async () => {
|
||||||
@@ -45,7 +55,7 @@ describe("AuthService", () => {
|
|||||||
|
|
||||||
const result = await AuthService.register("test@example.com", "password123")
|
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",
|
email: "test@example.com",
|
||||||
password: "password123",
|
password: "password123",
|
||||||
})
|
})
|
||||||
@@ -54,7 +64,9 @@ describe("AuthService", () => {
|
|||||||
|
|
||||||
it("logs out and clears the token", () => {
|
it("logs out and clears the token", () => {
|
||||||
localStorage.setItem("token", "some-token")
|
localStorage.setItem("token", "some-token")
|
||||||
|
expect(AuthService.isAuthenticated()).toBe(true)
|
||||||
AuthService.logout()
|
AuthService.logout()
|
||||||
expect(localStorage.getItem("token")).toBeNull()
|
expect(localStorage.getItem("token")).toBeNull()
|
||||||
|
expect(AuthService.isAuthenticated()).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
47
frontend/src/services/auth.ts
Normal file
47
frontend/src/services/auth.ts
Normal 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()
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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(() => {
|
afterEach(() => {
|
||||||
cleanup()
|
cleanup()
|
||||||
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/// <reference types="vitest" />
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
Reference in New Issue
Block a user