frontend work !
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 1m21s

This commit is contained in:
2025-11-22 13:36:04 -08:00
parent 81eb802f94
commit f43db7f82e
3 changed files with 108 additions and 20 deletions

View File

@@ -119,6 +119,9 @@ jobs:
GITHUB_CLIENT_SECRET: "dummy_github_secret" GITHUB_CLIENT_SECRET: "dummy_github_secret"
FRONTEND_URL: "http://localhost:3000" FRONTEND_URL: "http://localhost:3000"
# ✅ NEW: Provide the full backend URL for the Node.js test environment
VITE_API_BASE_URL: "http://localhost:3001/api"
# ✅ NEW: Backend requires this to start # ✅ NEW: Backend requires this to start
GEMINI_API_KEY: ${{ secrets.VITE_GOOGLE_GENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.VITE_GOOGLE_GENAI_API_KEY }}

View File

@@ -0,0 +1,87 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as apiClient from '../services/apiClient';
import type { User, Profile } from '../types';
/**
* @vitest-environment node
*/
/**
* A helper function to create a new user and log them in, returning the user object and auth token.
* This provides an authenticated context for testing protected API endpoints.
*/
const createAndLoginUser = async (email: string, password = 'password123') => {
// Register the new user.
await apiClient.registerUser(email, password, 'Test User');
// Log in to get the auth token.
const { user, token } = await apiClient.loginUser(email, password, false);
return { user, token };
};
describe('User API Routes Integration Tests', () => {
let testUser: User;
let authToken: string;
// Before any tests run, create a new user and log them in.
// The token will be used for all subsequent API calls in this test suite.
beforeAll(async () => {
const email = `user-test-${Date.now()}@example.com`;
const { user, token } = await createAndLoginUser(email);
testUser = user;
authToken = token;
});
// After all tests, clean up by deleting the created user.
afterAll(async () => {
if (testUser) {
// This requires an authenticated call to delete the account.
await apiClient.deleteUserAccount('password123', authToken);
}
});
it('should fetch the authenticated user profile via GET /api/users/profile', async () => {
// Act: Call the API endpoint using the authenticated token.
const profile = await apiClient.getAuthenticatedUserProfile(authToken);
// Assert: Verify the profile data matches the created user.
expect(profile).toBeDefined();
expect(profile.id).toBe(testUser.id);
expect(profile.user.email).toBe(testUser.email);
expect(profile.full_name).toBe('Test User');
expect(profile.role).toBe('user');
});
it('should update the user profile via PUT /api/users/profile', async () => {
// Arrange: Define the profile updates.
const profileUpdates = {
full_name: 'Updated Test User',
};
// Act: Call the update endpoint with the new data and the auth token.
const updatedProfile = await apiClient.updateUserProfile(profileUpdates, authToken);
// Assert: Check that the returned profile reflects the changes.
expect(updatedProfile).toBeDefined();
expect(updatedProfile.full_name).toBe('Updated Test User');
// Also, fetch the profile again to ensure the change was persisted.
const refetchedProfile = await apiClient.getAuthenticatedUserProfile(authToken);
expect(refetchedProfile.full_name).toBe('Updated Test User');
});
it('should update user preferences via PUT /api/users/profile/preferences', async () => {
// Arrange: Define the preference updates.
const preferenceUpdates = {
darkMode: true,
};
// Act: Call the update endpoint.
const updatedProfile = await apiClient.updateUserPreferences(preferenceUpdates, authToken);
// Assert: Check that the preferences object in the returned profile is updated.
expect(updatedProfile).toBeDefined();
expect(updatedProfile.preferences).toBeDefined();
expect(updatedProfile.preferences?.darkMode).toBe(true);
});
});

View File

@@ -57,10 +57,12 @@ const refreshToken = async (): Promise<string> => {
* @param options The fetch options. * @param options The fetch options.
* @returns A promise that resolves to the fetch Response. * @returns A promise that resolves to the fetch Response.
*/ */
export const apiFetch = async (url: string, options: RequestInit = {}): Promise<Response> => { export const apiFetch = async (url: string, options: RequestInit = {}, tokenOverride?: string): Promise<Response> => {
// Create a new headers object to avoid mutating the original options. // Create a new headers object to avoid mutating the original options.
const headers = new Headers(options.headers || {}); const headers = new Headers(options.headers || {});
const token = localStorage.getItem('authToken'); // Use the token override if provided (for testing), otherwise get it from localStorage.
// The `typeof window` check prevents errors in the Node.js test environment.
const token = tokenOverride ?? (typeof window !== 'undefined' ? localStorage.getItem('authToken') : null);
if (token) { if (token) {
headers.set('Authorization', `Bearer ${token}`); headers.set('Authorization', `Bearer ${token}`);
} }
@@ -450,16 +452,12 @@ export const removeShoppingListItem = async (itemId: number): Promise<void> => {
* @returns A promise that resolves to the user's combined UserProfile object. * @returns A promise that resolves to the user's combined UserProfile object.
* @throws An error if the request fails or if the user is not authenticated. * @throws An error if the request fails or if the user is not authenticated.
*/ */
export const getAuthenticatedUserProfile = async (): Promise<UserProfile> => { export const getAuthenticatedUserProfile = async (tokenOverride?: string): Promise<UserProfile> => {
const token = localStorage.getItem('authToken'); // The token is now passed to apiFetch, which handles the Authorization header.
// If no token is provided (in browser context), apiFetch will get it from localStorage.
if (!token) {
throw new Error('Authentication token not found.');
}
const response = await apiFetch(`${API_BASE_URL}/users/profile`, { const response = await apiFetch(`${API_BASE_URL}/users/profile`, {
method: 'GET', method: 'GET',
}); }, tokenOverride);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) throw new Error('Session expired. Please log in again.'); if (response.status === 401) throw new Error('Session expired. Please log in again.');
@@ -1087,12 +1085,12 @@ export async function resetPassword(token: string, newPassword: string): Promise
* @param preferences A partial object of the user's preferences to update. * @param preferences A partial object of the user's preferences to update.
* @returns A promise that resolves to the user's full, updated profile object. * @returns A promise that resolves to the user's full, updated profile object.
*/ */
export async function updateUserPreferences(preferences: Partial<Profile['preferences']>): Promise<Profile> { export async function updateUserPreferences(preferences: Partial<Profile['preferences']>, tokenOverride?: string): Promise<Profile> {
const response = await apiFetch(`${API_BASE_URL}/users/profile/preferences`, { const response = await apiFetch(`${API_BASE_URL}/users/profile/preferences`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(preferences), body: JSON.stringify(preferences),
}); }, tokenOverride);
const data = await response.json(); const data = await response.json();
@@ -1107,12 +1105,12 @@ export async function updateUserPreferences(preferences: Partial<Profile['prefer
* @param profileData An object containing the full_name and/or avatar_url to update. * @param profileData An object containing the full_name and/or avatar_url to update.
* @returns A promise that resolves to the user's full, updated profile object. * @returns A promise that resolves to the user's full, updated profile object.
*/ */
export async function updateUserProfile(profileData: { full_name?: string; avatar_url?: string }): Promise<Profile> { export async function updateUserProfile(profileData: { full_name?: string; avatar_url?: string }, tokenOverride?: string): Promise<Profile> {
const response = await apiFetch(`${API_BASE_URL}/users/profile`, { const response = await apiFetch(`${API_BASE_URL}/users/profile`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(profileData), body: JSON.stringify(profileData),
}); }, tokenOverride);
const data = await response.json(); const data = await response.json();
@@ -1126,10 +1124,10 @@ export async function updateUserProfile(profileData: { full_name?: string; avata
* Fetches a complete export of the user's data from the backend. * Fetches a complete export of the user's data from the backend.
* @returns A promise that resolves to a JSON object of the user's data. * @returns A promise that resolves to a JSON object of the user's data.
*/ */
export async function exportUserData(): Promise<UserDataExport> { export async function exportUserData(tokenOverride?: string): Promise<UserDataExport> {
const response = await apiFetch(`${API_BASE_URL}/users/data-export`, { const response = await apiFetch(`${API_BASE_URL}/users/data-export`, {
method: 'GET', method: 'GET',
}); }, tokenOverride);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Failed to export user data.' })); const errorData = await response.json().catch(() => ({ message: 'Failed to export user data.' }));
@@ -1174,12 +1172,12 @@ export const setUserAppliances = async (applianceIds: number[]): Promise<void> =
* @param newPassword The user's new password. * @param newPassword The user's new password.
* @returns A promise that resolves on success. * @returns A promise that resolves on success.
*/ */
export async function updateUserPassword(newPassword: string): Promise<{ message: string }> { export async function updateUserPassword(newPassword: string, tokenOverride?: string): Promise<{ message: string }> {
const response = await apiFetch(`${API_BASE_URL}/users/profile/password`, { const response = await apiFetch(`${API_BASE_URL}/users/profile/password`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newPassword }), body: JSON.stringify({ newPassword }),
}); }, tokenOverride);
const data = await response.json(); const data = await response.json();
if (!response.ok) { if (!response.ok) {
@@ -1193,12 +1191,12 @@ export async function updateUserPassword(newPassword: string): Promise<{ message
* @param password The user's current password for verification. * @param password The user's current password for verification.
* @returns A promise that resolves on success. * @returns A promise that resolves on success.
*/ */
export async function deleteUserAccount(password: string): Promise<{ message: string }> { export async function deleteUserAccount(password: string, tokenOverride?: string): Promise<{ message: string }> {
const response = await apiFetch(`${API_BASE_URL}/users/account`, { const response = await apiFetch(`${API_BASE_URL}/users/account`, {
method: 'DELETE', method: 'DELETE',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }), body: JSON.stringify({ password }),
}); }, tokenOverride);
const data = await response.json(); const data = await response.json();
if (!response.ok) { if (!response.ok) {