frontend work !
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 1m21s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 1m21s
This commit is contained in:
@@ -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 }}
|
||||||
|
|
||||||
|
|||||||
87
src/routes/user.integration.test.ts
Normal file
87
src/routes/user.integration.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user