more integration tests added
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 2m43s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 2m43s
This commit is contained in:
@@ -368,12 +368,12 @@ export const countFlyerItemsForFlyers = async (flyerIds: number[]): Promise<numb
|
||||
* @param logoImage The logo image file.
|
||||
* @returns A promise that resolves with the new logo URL.
|
||||
*/
|
||||
export const uploadLogoAndUpdateStore = async (storeId: number, logoImage: File): Promise<{ logoUrl: string }> => {
|
||||
export const uploadLogoAndUpdateStore = async (storeId: number, logoImage: File, tokenOverride?: string): Promise<{ logoUrl: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append('logoImage', logoImage);
|
||||
|
||||
// Use apiFetch to ensure the user is authenticated to perform this action.
|
||||
const response = await apiFetch(`${API_BASE_URL}/stores/${storeId}/logo`, {
|
||||
const response = await apiFetch(`/stores/${storeId}/logo`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
// Do not set Content-Type for FormData, browser handles it.
|
||||
@@ -388,12 +388,12 @@ export const uploadLogoAndUpdateStore = async (storeId: number, logoImage: File)
|
||||
* @param logoImage The logo image file.
|
||||
* @returns A promise that resolves with the new logo URL.
|
||||
*/
|
||||
export const uploadBrandLogo = async (brandId: number, logoImage: File): Promise<{ logoUrl: string }> => {
|
||||
export const uploadBrandLogo = async (brandId: number, logoImage: File, tokenOverride?: string): Promise<{ logoUrl: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append('logoImage', logoImage);
|
||||
|
||||
// Use apiFetch to ensure the user is an authenticated admin.
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/brands/${brandId}/logo`, {
|
||||
const response = await apiFetch(`/admin/brands/${brandId}/logo`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
// Do not set Content-Type for FormData, browser handles it.
|
||||
@@ -408,12 +408,12 @@ export const uploadBrandLogo = async (brandId: number, logoImage: File): Promise
|
||||
* @param masterItemIds An array of master grocery item IDs.
|
||||
* @returns A promise that resolves to an array of historical price records.
|
||||
*/
|
||||
export const fetchHistoricalPriceData = async (masterItemIds: number[]): Promise<{ master_item_id: number; summary_date: string; avg_price_in_cents: number | null; }[]> => {
|
||||
export const fetchHistoricalPriceData = async (masterItemIds: number[], tokenOverride?: string): Promise<{ master_item_id: number; summary_date: string; avg_price_in_cents: number | null; }[]> => {
|
||||
if (masterItemIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const response = await apiFetch(`${API_BASE_URL}/price-history`, {
|
||||
const response = await apiFetch(`/price-history`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ masterItemIds }),
|
||||
@@ -429,8 +429,8 @@ export const fetchHistoricalPriceData = async (masterItemIds: number[]): Promise
|
||||
|
||||
// --- Watched Items API Functions ---
|
||||
|
||||
export const fetchWatchedItems = async (): Promise<MasterGroceryItem[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/watched-items`);
|
||||
export const fetchWatchedItems = async (tokenOverride?: string): Promise<MasterGroceryItem[]> => {
|
||||
const response = await apiFetch(`/watched-items`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch watched items.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -438,12 +438,12 @@ export const fetchWatchedItems = async (): Promise<MasterGroceryItem[]> => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const addWatchedItem = async (itemName: string, category: string): Promise<MasterGroceryItem> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/watched-items`, {
|
||||
export const addWatchedItem = async (itemName: string, category: string, tokenOverride?: string): Promise<MasterGroceryItem> => {
|
||||
const response = await apiFetch(`/watched-items`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ itemName, category }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to add watched item.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -451,10 +451,10 @@ export const addWatchedItem = async (itemName: string, category: string): Promis
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const removeWatchedItem = async (masterItemId: number): Promise<void> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/watched-items/${masterItemId}`, {
|
||||
export const removeWatchedItem = async (masterItemId: number, tokenOverride?: string): Promise<void> => {
|
||||
const response = await apiFetch(`/watched-items/${masterItemId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to remove watched item.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -463,8 +463,8 @@ export const removeWatchedItem = async (masterItemId: number): Promise<void> =>
|
||||
|
||||
// --- Shopping List API Functions ---
|
||||
|
||||
export const fetchShoppingLists = async (): Promise<ShoppingList[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/shopping-lists`);
|
||||
export const fetchShoppingLists = async (tokenOverride?: string): Promise<ShoppingList[]> => {
|
||||
const response = await apiFetch(`/shopping-lists`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch shopping lists.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -472,12 +472,12 @@ export const fetchShoppingLists = async (): Promise<ShoppingList[]> => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const createShoppingList = async (name: string): Promise<ShoppingList> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/shopping-lists`, {
|
||||
export const createShoppingList = async (name: string, tokenOverride?: string): Promise<ShoppingList> => {
|
||||
const response = await apiFetch(`/shopping-lists`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to create shopping list.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -485,22 +485,22 @@ export const createShoppingList = async (name: string): Promise<ShoppingList> =>
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const deleteShoppingList = async (listId: number): Promise<void> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/shopping-lists/${listId}`, {
|
||||
export const deleteShoppingList = async (listId: number, tokenOverride?: string): Promise<void> => {
|
||||
const response = await apiFetch(`/shopping-lists/${listId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to delete shopping list.' }));
|
||||
throw new Error(errorData.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const addShoppingListItem = async (listId: number, item: { masterItemId?: number, customItemName?: string }): Promise<ShoppingListItem> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/shopping-lists/${listId}/items`, {
|
||||
export const addShoppingListItem = async (listId: number, item: { masterItemId?: number, customItemName?: string }, tokenOverride?: string): Promise<ShoppingListItem> => {
|
||||
const response = await apiFetch(`/shopping-lists/${listId}/items`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to add item to list.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -508,12 +508,12 @@ export const addShoppingListItem = async (listId: number, item: { masterItemId?:
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const updateShoppingListItem = async (itemId: number, updates: Partial<ShoppingListItem>): Promise<ShoppingListItem> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/shopping-lists/items/${itemId}`, {
|
||||
export const updateShoppingListItem = async (itemId: number, updates: Partial<ShoppingListItem>, tokenOverride?: string): Promise<ShoppingListItem> => {
|
||||
const response = await apiFetch(`/shopping-lists/items/${itemId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to update list item.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -521,10 +521,10 @@ export const updateShoppingListItem = async (itemId: number, updates: Partial<Sh
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const removeShoppingListItem = async (itemId: number): Promise<void> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/shopping-lists/items/${itemId}`, {
|
||||
export const removeShoppingListItem = async (itemId: number, tokenOverride?: string): Promise<void> => {
|
||||
const response = await apiFetch(`/shopping-lists/items/${itemId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to remove list item.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -581,13 +581,13 @@ export async function loginUser(email: string, password: string, rememberMe: boo
|
||||
* @param receiptImage The image file of the receipt.
|
||||
* @returns A promise that resolves with the backend's response, including the newly created receipt record.
|
||||
*/
|
||||
export const uploadReceipt = async (receiptImage: File): Promise<{ message: string; receipt: Receipt }> => {
|
||||
export const uploadReceipt = async (receiptImage: File, tokenOverride?: string): Promise<{ message: string; receipt: Receipt }> => {
|
||||
const formData = new FormData();
|
||||
formData.append('receiptImage', receiptImage);
|
||||
|
||||
// Use apiFetch to ensure the user is authenticated for this action.
|
||||
// The browser will automatically set the correct 'Content-Type' for FormData.
|
||||
const response = await apiFetch(`${API_BASE_URL}/receipts/upload`, {
|
||||
const response = await apiFetch(`/receipts/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
@@ -604,8 +604,8 @@ export const uploadReceipt = async (receiptImage: File): Promise<{ message: stri
|
||||
* @param receiptId The ID of the processed receipt.
|
||||
* @returns A promise that resolves to an array of ReceiptDeal objects.
|
||||
*/
|
||||
export const getDealsForReceipt = async (receiptId: number): Promise<ReceiptDeal[]> => {
|
||||
const response = await apiFetch(`/receipts/${receiptId}/deals`);
|
||||
export const getDealsForReceipt = async (receiptId: number, tokenOverride?: string): Promise<ReceiptDeal[]> => {
|
||||
const response = await apiFetch(`/receipts/${receiptId}/deals`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch deals for receipt.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -641,9 +641,9 @@ export const trackFlyerItemInteraction = async (itemId: number, type: 'view' | '
|
||||
* This is a "fire-and-forget" call.
|
||||
* @param query The search query data to log.
|
||||
*/
|
||||
export const logSearchQuery = async (query: Omit<SearchQuery, 'id' | 'created_at' | 'user_id'>): Promise<void> => {
|
||||
export const logSearchQuery = async (query: Omit<SearchQuery, 'id' | 'created_at' | 'user_id'>, tokenOverride?: string): Promise<void> => {
|
||||
try {
|
||||
apiFetch(`${API_BASE_URL}/search/log`, {
|
||||
apiFetch(`/search/log`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(query),
|
||||
@@ -658,8 +658,8 @@ export const logSearchQuery = async (query: Omit<SearchQuery, 'id' | 'created_at
|
||||
* Fetches all pantry locations for the current user.
|
||||
* @returns A promise that resolves to an array of PantryLocation objects.
|
||||
*/
|
||||
export const getPantryLocations = async (): Promise<PantryLocation[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/pantry/locations`);
|
||||
export const getPantryLocations = async (tokenOverride?: string): Promise<PantryLocation[]> => {
|
||||
const response = await apiFetch(`/pantry/locations`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch pantry locations.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -672,12 +672,12 @@ export const getPantryLocations = async (): Promise<PantryLocation[]> => {
|
||||
* @param name The name of the new location (e.g., "Fridge").
|
||||
* @returns A promise that resolves to the newly created PantryLocation object.
|
||||
*/
|
||||
export const createPantryLocation = async (name: string): Promise<PantryLocation> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/pantry/locations`, {
|
||||
export const createPantryLocation = async (name: string, tokenOverride?: string): Promise<PantryLocation> => {
|
||||
const response = await apiFetch(`/pantry/locations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to create pantry location.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -691,12 +691,12 @@ export const createPantryLocation = async (name: string): Promise<PantryLocation
|
||||
* @param totalSpentCents Optional total amount spent on the trip.
|
||||
* @returns A promise that resolves to an object containing the new shopping trip ID.
|
||||
*/
|
||||
export const completeShoppingList = async (shoppingListId: number, totalSpentCents?: number): Promise<{ newTripId: number }> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/shopping-lists/${shoppingListId}/complete`, {
|
||||
export const completeShoppingList = async (shoppingListId: number, totalSpentCents?: number, tokenOverride?: string): Promise<{ newTripId: number }> => {
|
||||
const response = await apiFetch(`/shopping-lists/${shoppingListId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ totalSpentCents }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to complete shopping list.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -708,8 +708,8 @@ export const completeShoppingList = async (shoppingListId: number, totalSpentCen
|
||||
* Fetches the historical shopping trips for the current user.
|
||||
* @returns A promise that resolves to an array of ShoppingTrip objects.
|
||||
*/
|
||||
export const getShoppingTripHistory = async (): Promise<ShoppingTrip[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/shopping-history`);
|
||||
export const getShoppingTripHistory = async (tokenOverride?: string): Promise<ShoppingTrip[]> => {
|
||||
const response = await apiFetch(`/shopping-history`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch shopping trip history.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -749,8 +749,8 @@ export const getAppliances = async (): Promise<Appliance[]> => {
|
||||
* Fetches the dietary restrictions for the currently authenticated user.
|
||||
* @returns A promise that resolves to an array of the user's DietaryRestriction objects.
|
||||
*/
|
||||
export const getUserDietaryRestrictions = async (): Promise<DietaryRestriction[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/me/dietary-restrictions`);
|
||||
export const getUserDietaryRestrictions = async (tokenOverride?: string): Promise<DietaryRestriction[]> => {
|
||||
const response = await apiFetch(`/users/me/dietary-restrictions`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch user dietary restrictions.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -763,12 +763,12 @@ export const getUserDietaryRestrictions = async (): Promise<DietaryRestriction[]
|
||||
* This will replace all existing restrictions with the new set.
|
||||
* @param restrictionIds An array of numbers representing the IDs of the selected restrictions.
|
||||
*/
|
||||
export const setUserDietaryRestrictions = async (restrictionIds: number[]): Promise<void> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/me/dietary-restrictions`, {
|
||||
export const setUserDietaryRestrictions = async (restrictionIds: number[], tokenOverride?: string): Promise<void> => {
|
||||
const response = await apiFetch(`/users/me/dietary-restrictions`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ restrictionIds }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to set user dietary restrictions.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -779,8 +779,8 @@ export const setUserDietaryRestrictions = async (restrictionIds: number[]): Prom
|
||||
* Fetches recipes that are compatible with the user's dietary restrictions.
|
||||
* @returns A promise that resolves to an array of compatible Recipe objects.
|
||||
*/
|
||||
export const getCompatibleRecipes = async (): Promise<Recipe[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/me/compatible-recipes`);
|
||||
export const getCompatibleRecipes = async (tokenOverride?: string): Promise<Recipe[]> => {
|
||||
const response = await apiFetch(`/users/me/compatible-recipes`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch compatible recipes.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -794,8 +794,8 @@ export const getCompatibleRecipes = async (): Promise<Recipe[]> => {
|
||||
* @param offset The starting offset for pagination.
|
||||
* @returns A promise that resolves to an array of ActivityLogItem objects.
|
||||
*/
|
||||
export const getUserFeed = async (limit: number = 20, offset: number = 0): Promise<ActivityLogItem[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/me/feed?limit=${limit}&offset=${offset}`);
|
||||
export const getUserFeed = async (limit: number = 20, offset: number = 0, tokenOverride?: string): Promise<ActivityLogItem[]> => {
|
||||
const response = await apiFetch(`/users/me/feed?limit=${limit}&offset=${offset}`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch user feed.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -808,10 +808,10 @@ export const getUserFeed = async (limit: number = 20, offset: number = 0): Promi
|
||||
* @param originalRecipeId The ID of the recipe to fork.
|
||||
* @returns A promise that resolves to the newly created Recipe object.
|
||||
*/
|
||||
export const forkRecipe = async (originalRecipeId: number): Promise<Recipe> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/recipes/${originalRecipeId}/fork`, {
|
||||
export const forkRecipe = async (originalRecipeId: number, tokenOverride?: string): Promise<Recipe> => {
|
||||
const response = await apiFetch(`/recipes/${originalRecipeId}/fork`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fork recipe.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -823,10 +823,10 @@ export const forkRecipe = async (originalRecipeId: number): Promise<Recipe> => {
|
||||
* Follows another user.
|
||||
* @param userIdToFollow The UUID of the user to follow.
|
||||
*/
|
||||
export const followUser = async (userIdToFollow: string): Promise<void> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/${userIdToFollow}/follow`, {
|
||||
export const followUser = async (userIdToFollow: string, tokenOverride?: string): Promise<void> => {
|
||||
const response = await apiFetch(`/users/${userIdToFollow}/follow`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to follow user.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -837,10 +837,10 @@ export const followUser = async (userIdToFollow: string): Promise<void> => {
|
||||
* Unfollows another user.
|
||||
* @param userIdToUnfollow The UUID of the user to unfollow.
|
||||
*/
|
||||
export const unfollowUser = async (userIdToUnfollow: string): Promise<void> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/${userIdToUnfollow}/follow`, {
|
||||
export const unfollowUser = async (userIdToUnfollow: string, tokenOverride?: string): Promise<void> => {
|
||||
const response = await apiFetch(`/users/${userIdToUnfollow}/follow`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to unfollow user.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -855,8 +855,8 @@ export const unfollowUser = async (userIdToUnfollow: string): Promise<void> => {
|
||||
* @param offset The starting offset for pagination.
|
||||
* @returns A promise that resolves to an array of ActivityLogItem objects.
|
||||
*/
|
||||
export const fetchActivityLog = async (limit: number = 20, offset: number = 0): Promise<ActivityLogItem[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/activity-log?limit=${limit}&offset=${offset}`);
|
||||
export const fetchActivityLog = async (limit: number = 20, offset: number = 0, tokenOverride?: string): Promise<ActivityLogItem[]> => {
|
||||
const response = await apiFetch(`/activity-log?limit=${limit}&offset=${offset}`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch activity log.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -866,8 +866,8 @@ export const fetchActivityLog = async (limit: number = 20, offset: number = 0):
|
||||
|
||||
// --- Favorite Recipes API Functions ---
|
||||
|
||||
export const getUserFavoriteRecipes = async (): Promise<Recipe[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/favorite-recipes`);
|
||||
export const getUserFavoriteRecipes = async (tokenOverride?: string): Promise<Recipe[]> => {
|
||||
const response = await apiFetch(`/users/favorite-recipes`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch favorite recipes.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -875,12 +875,12 @@ export const getUserFavoriteRecipes = async (): Promise<Recipe[]> => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const addFavoriteRecipe = async (recipeId: number): Promise<FavoriteRecipe> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/favorite-recipes`, {
|
||||
export const addFavoriteRecipe = async (recipeId: number, tokenOverride?: string): Promise<FavoriteRecipe> => {
|
||||
const response = await apiFetch(`/users/favorite-recipes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ recipeId }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to add favorite recipe.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -888,10 +888,10 @@ export const addFavoriteRecipe = async (recipeId: number): Promise<FavoriteRecip
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const removeFavoriteRecipe = async (recipeId: number): Promise<void> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/favorite-recipes/${recipeId}`, {
|
||||
export const removeFavoriteRecipe = async (recipeId: number, tokenOverride?: string): Promise<void> => {
|
||||
const response = await apiFetch(`/users/favorite-recipes/${recipeId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to remove favorite recipe.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -910,12 +910,12 @@ export const getRecipeComments = async (recipeId: number): Promise<RecipeComment
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const addRecipeComment = async (recipeId: number, content: string, parentCommentId?: number): Promise<RecipeComment> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/recipes/${recipeId}/comments`, {
|
||||
export const addRecipeComment = async (recipeId: number, content: string, parentCommentId?: number, tokenOverride?: string): Promise<RecipeComment> => {
|
||||
const response = await apiFetch(`/recipes/${recipeId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, parentCommentId }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to add recipe comment.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -946,12 +946,12 @@ export const getUnmatchedFlyerItems = async (): Promise<UnmatchedFlyerItem[]> =>
|
||||
* @param status The new status for the recipe.
|
||||
* @returns A promise that resolves to the updated Recipe object.
|
||||
*/
|
||||
export const updateRecipeStatus = async (recipeId: number, status: 'private' | 'pending_review' | 'public' | 'rejected'): Promise<Recipe> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/recipes/${recipeId}/status`, {
|
||||
export const updateRecipeStatus = async (recipeId: number, status: 'private' | 'pending_review' | 'public' | 'rejected', tokenOverride?: string): Promise<Recipe> => {
|
||||
const response = await apiFetch(`/admin/recipes/${recipeId}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to update recipe status.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -966,12 +966,12 @@ export const updateRecipeStatus = async (recipeId: number, status: 'private' | '
|
||||
* @param status The new status for the comment.
|
||||
* @returns A promise that resolves to the updated RecipeComment object.
|
||||
*/
|
||||
export const updateRecipeCommentStatus = async (commentId: number, status: 'visible' | 'hidden' | 'reported'): Promise<RecipeComment> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/comments/${commentId}/status`, {
|
||||
export const updateRecipeCommentStatus = async (commentId: number, status: 'visible' | 'hidden' | 'reported', tokenOverride?: string): Promise<RecipeComment> => {
|
||||
const response = await apiFetch(`/admin/comments/${commentId}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to update recipe comment status.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -983,8 +983,8 @@ export const updateRecipeCommentStatus = async (commentId: number, status: 'visi
|
||||
* Fetches all brands from the backend. Requires admin privileges.
|
||||
* @returns A promise that resolves to an array of Brand objects.
|
||||
*/
|
||||
export const fetchAllBrands = async (): Promise<Brand[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/brands`);
|
||||
export const fetchAllBrands = async (tokenOverride?: string): Promise<Brand[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/brands`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch brands.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -1009,8 +1009,8 @@ export interface DailyStat {
|
||||
* Fetches daily user registration and flyer upload stats for the last 30 days.
|
||||
* @returns A promise that resolves to an array of daily stat objects.
|
||||
*/
|
||||
export const getDailyStats = async (): Promise<DailyStat[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/stats/daily`);
|
||||
export const getDailyStats = async (tokenOverride?: string): Promise<DailyStat[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/stats/daily`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch daily statistics.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -1021,8 +1021,8 @@ export const getDailyStats = async (): Promise<DailyStat[]> => {
|
||||
* Fetches application-wide statistics. Requires admin privileges.
|
||||
* @returns A promise that resolves to an object containing app stats.
|
||||
*/
|
||||
export const getApplicationStats = async (): Promise<AppStats> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/stats`);
|
||||
export const getApplicationStats = async (tokenOverride?: string): Promise<AppStats> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/stats`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch application statistics.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -1036,8 +1036,8 @@ export const getApplicationStats = async (): Promise<AppStats> => {
|
||||
* Fetches all pending suggested corrections. Requires admin privileges.
|
||||
* @returns A promise that resolves to an array of SuggestedCorrection objects.
|
||||
*/
|
||||
export const getSuggestedCorrections = async (): Promise<SuggestedCorrection[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/corrections`);
|
||||
export const getSuggestedCorrections = async (tokenOverride?: string): Promise<SuggestedCorrection[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/corrections`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch suggested corrections.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -1050,10 +1050,10 @@ export const getSuggestedCorrections = async (): Promise<SuggestedCorrection[]>
|
||||
* @param correctionId The ID of the correction to approve.
|
||||
* @returns A promise that resolves with the success message.
|
||||
*/
|
||||
export const approveCorrection = async (correctionId: number): Promise<{ message: string }> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/corrections/${correctionId}/approve`, {
|
||||
export const approveCorrection = async (correctionId: number, tokenOverride?: string): Promise<{ message: string }> => {
|
||||
const response = await apiFetch(`/admin/corrections/${correctionId}/approve`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to approve correction.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -1065,10 +1065,10 @@ export const approveCorrection = async (correctionId: number): Promise<{ message
|
||||
* Rejects a suggested correction. Requires admin privileges.
|
||||
* @param correctionId The ID of the correction to reject.
|
||||
*/
|
||||
export const rejectCorrection = async (correctionId: number): Promise<{ message: string }> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/corrections/${correctionId}/reject`, {
|
||||
export const rejectCorrection = async (correctionId: number, tokenOverride?: string): Promise<{ message: string }> => {
|
||||
const response = await apiFetch(`/admin/corrections/${correctionId}/reject`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to reject correction.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -1082,12 +1082,12 @@ export const rejectCorrection = async (correctionId: number): Promise<{ message:
|
||||
* @param newSuggestedValue The new value for the suggestion.
|
||||
* @returns A promise that resolves to the updated SuggestedCorrection object.
|
||||
*/
|
||||
export const updateSuggestedCorrection = async (correctionId: number, newSuggestedValue: string): Promise<SuggestedCorrection> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/admin/corrections/${correctionId}`, {
|
||||
export const updateSuggestedCorrection = async (correctionId: number, newSuggestedValue: string, tokenOverride?: string): Promise<SuggestedCorrection> => {
|
||||
const response = await apiFetch(`/admin/corrections/${correctionId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ suggested_value: newSuggestedValue }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to update suggested correction.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -1226,8 +1226,8 @@ export async function exportUserData(tokenOverride?: string): Promise<UserDataEx
|
||||
* Fetches the kitchen appliances for the currently authenticated user.
|
||||
* @returns A promise that resolves to an array of the user's Appliance objects.
|
||||
*/
|
||||
export const getUserAppliances = async (): Promise<Appliance[]> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/me/appliances`);
|
||||
export const getUserAppliances = async (tokenOverride?: string): Promise<Appliance[]> => {
|
||||
const response = await apiFetch(`/users/me/appliances`, {}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to fetch user appliances.' }));
|
||||
throw new Error(errorData.message);
|
||||
@@ -1240,12 +1240,12 @@ export const getUserAppliances = async (): Promise<Appliance[]> => {
|
||||
* This will replace all existing appliances with the new set.
|
||||
* @param applianceIds An array of numbers representing the IDs of the selected appliances.
|
||||
*/
|
||||
export const setUserAppliances = async (applianceIds: number[]): Promise<void> => {
|
||||
const response = await apiFetch(`${API_BASE_URL}/users/me/appliances`, {
|
||||
export const setUserAppliances = async (applianceIds: number[], tokenOverride?: string): Promise<void> => {
|
||||
const response = await apiFetch(`/users/me/appliances`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ applianceIds }),
|
||||
});
|
||||
}, tokenOverride);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to set user appliances.' }));
|
||||
throw new Error(errorData.message);
|
||||
|
||||
111
src/tests/integration/admin.integration.test.ts
Normal file
111
src/tests/integration/admin.integration.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
// src/tests/integration/admin.integration.test.ts
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import { getPool } from '../../services/db/connection';
|
||||
import type { User } from '../../types';
|
||||
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
const TEST_PASSWORD = 'a-much-stronger-password-for-testing-!@#$';
|
||||
|
||||
describe('Admin API Routes Integration Tests', () => {
|
||||
let adminUser: User;
|
||||
let adminToken: string;
|
||||
let regularUser: User;
|
||||
let regularUserToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Log in as the pre-seeded admin user
|
||||
const adminLoginResponse = await apiClient.loginUser('admin@example.com', 'adminpass', false);
|
||||
adminUser = adminLoginResponse.user;
|
||||
adminToken = adminLoginResponse.token;
|
||||
|
||||
// Create and log in as a new regular user for permission testing
|
||||
const regularUserEmail = `regular-user-${Date.now()}@example.com`;
|
||||
await apiClient.registerUser(regularUserEmail, TEST_PASSWORD, 'Regular User');
|
||||
const regularUserLoginResponse = await apiClient.loginUser(regularUserEmail, TEST_PASSWORD, false);
|
||||
regularUser = regularUserLoginResponse.user;
|
||||
regularUserToken = regularUserLoginResponse.token;
|
||||
|
||||
// Cleanup the created user after all tests in this file are done
|
||||
return async () => {
|
||||
if (regularUser) {
|
||||
await getPool().query('DELETE FROM public.users WHERE user_id = $1', [regularUser.user_id]);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('GET /api/admin/stats', () => {
|
||||
it('should allow an admin to fetch application stats', async () => {
|
||||
const stats = await apiClient.getApplicationStats(adminToken);
|
||||
expect(stats).toBeDefined();
|
||||
expect(stats).toHaveProperty('flyerCount');
|
||||
expect(stats).toHaveProperty('userCount');
|
||||
expect(stats).toHaveProperty('flyerItemCount');
|
||||
});
|
||||
|
||||
it('should forbid a regular user from fetching application stats', async () => {
|
||||
await expect(apiClient.getApplicationStats(regularUserToken)).rejects.toThrow(
|
||||
'Forbidden: Administrator access required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/admin/stats/daily', () => {
|
||||
it('should allow an admin to fetch daily stats', async () => {
|
||||
const dailyStats = await apiClient.getDailyStats(adminToken);
|
||||
expect(dailyStats).toBeDefined();
|
||||
expect(Array.isArray(dailyStats)).toBe(true);
|
||||
// The seed script creates users, so we should have some data
|
||||
expect(dailyStats.length).toBe(30);
|
||||
expect(dailyStats[0]).toHaveProperty('date');
|
||||
expect(dailyStats[0]).toHaveProperty('new_users');
|
||||
expect(dailyStats[0]).toHaveProperty('new_flyers');
|
||||
});
|
||||
|
||||
it('should forbid a regular user from fetching daily stats', async () => {
|
||||
await expect(apiClient.getDailyStats(regularUserToken)).rejects.toThrow(
|
||||
'Forbidden: Administrator access required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/admin/corrections', () => {
|
||||
it('should allow an admin to fetch suggested corrections', async () => {
|
||||
// This test just verifies access and correct response shape.
|
||||
// More detailed tests would require seeding corrections.
|
||||
const corrections = await apiClient.getSuggestedCorrections(adminToken);
|
||||
expect(corrections).toBeDefined();
|
||||
expect(Array.isArray(corrections)).toBe(true);
|
||||
});
|
||||
|
||||
it('should forbid a regular user from fetching suggested corrections', async () => {
|
||||
await expect(apiClient.getSuggestedCorrections(regularUserToken)).rejects.toThrow(
|
||||
'Forbidden: Administrator access required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/admin/brands', () => {
|
||||
it('should allow an admin to fetch all brands', async () => {
|
||||
const brands = await apiClient.fetchAllBrands(adminToken);
|
||||
expect(brands).toBeDefined();
|
||||
expect(Array.isArray(brands)).toBe(true);
|
||||
// The seed script creates brands
|
||||
expect(brands.length).toBeGreaterThan(0);
|
||||
expect(brands[0]).toHaveProperty('brand_id');
|
||||
expect(brands[0]).toHaveProperty('name');
|
||||
});
|
||||
|
||||
it('should forbid a regular user from fetching all brands', async () => {
|
||||
await expect(apiClient.fetchAllBrands(regularUserToken)).rejects.toThrow(
|
||||
'Forbidden: Administrator access required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add tests for POST/PUT/DELETE admin endpoints, which would require seeding specific data
|
||||
// (e.g., a correction to approve, a recipe to update status for).
|
||||
});
|
||||
77
src/tests/integration/ai.integration.test.ts
Normal file
77
src/tests/integration/ai.integration.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
// src/tests/integration/ai.integration.test.ts
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import * as aiApiClient from '../../services/aiApiClient';
|
||||
import type { User } from '../../types';
|
||||
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
const TEST_PASSWORD = 'a-much-stronger-password-for-testing-!@#$';
|
||||
|
||||
describe('AI API Routes Integration Tests', () => {
|
||||
let testUser: User;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create and log in as a new user for authenticated tests
|
||||
const email = `ai-test-user-${Date.now()}@example.com`;
|
||||
await apiClient.registerUser(email, TEST_PASSWORD, 'AI Tester');
|
||||
const loginResponse = await apiClient.loginUser(email, TEST_PASSWORD, false);
|
||||
testUser = loginResponse.user;
|
||||
authToken = loginResponse.token;
|
||||
});
|
||||
|
||||
it('POST /api/ai/check-flyer should return a boolean', async () => {
|
||||
const mockImageFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
||||
const result = await aiApiClient.isImageAFlyer(mockImageFile, authToken);
|
||||
// The backend is stubbed to always return true for this check
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('POST /api/ai/extract-address should return a stubbed address', async () => {
|
||||
const mockImageFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
||||
const result = await aiApiClient.extractAddressFromImage(mockImageFile, authToken);
|
||||
expect(result).toBe("123 AI Street, Server City");
|
||||
});
|
||||
|
||||
it('POST /api/ai/extract-logo should return a stubbed response', async () => {
|
||||
const mockImageFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
||||
const result = await aiApiClient.extractLogoFromImage([mockImageFile], authToken);
|
||||
expect(result).toEqual({ store_logo_base_64: null });
|
||||
});
|
||||
|
||||
it('POST /api/ai/quick-insights should return a stubbed insight', async () => {
|
||||
const result = await aiApiClient.getQuickInsights([], authToken);
|
||||
expect(result).toBe("This is a server-generated quick insight: buy the cheap stuff!");
|
||||
});
|
||||
|
||||
it('POST /api/ai/deep-dive should return a stubbed analysis', async () => {
|
||||
const result = await aiApiClient.getDeepDiveAnalysis([], authToken);
|
||||
expect(result).toBe("This is a server-generated deep dive analysis. It is very detailed.");
|
||||
});
|
||||
|
||||
it('POST /api/ai/search-web should return a stubbed search result', async () => {
|
||||
const result = await aiApiClient.searchWeb([], authToken);
|
||||
expect(result).toEqual({ text: "The web says this is good.", sources: [] });
|
||||
});
|
||||
|
||||
it('POST /api/ai/plan-trip should return a stubbed trip plan', async () => {
|
||||
const mockLocation = { latitude: 48.4284, longitude: -123.3656 };
|
||||
const result = await aiApiClient.planTripWithMaps([], undefined, mockLocation, authToken);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.text).toContain('trip plan');
|
||||
});
|
||||
|
||||
it('POST /api/ai/generate-image should reject because it is not implemented', async () => {
|
||||
// The backend for this is not stubbed and will throw an error.
|
||||
// This test confirms that the endpoint is protected and responds as expected to a failure.
|
||||
await expect(aiApiClient.generateImageFromText("a test prompt", authToken)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('POST /api/ai/generate-speech should reject because it is not implemented', async () => {
|
||||
// The backend for this is not stubbed and will throw an error.
|
||||
await expect(aiApiClient.generateSpeechFromText("a test prompt", authToken)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/tests/integration/auth.integration.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { loginUser } from '../../services/apiClient';
|
||||
import { getPool } from '../../services/db/connection';
|
||||
|
||||
@@ -14,6 +14,14 @@ import { getPool } from '../../services/db/connection';
|
||||
* To run only these tests: `vitest run src/tests/auth.integration.test.ts`
|
||||
*/
|
||||
describe('Authentication API Integration', () => {
|
||||
let refreshTokenCookie: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Log in once to get a valid refresh token cookie for the refresh test
|
||||
const response = await loginUser('admin@example.com', 'adminpass', true);
|
||||
// This is a simplified way to grab the cookie for testing purposes.
|
||||
refreshTokenCookie = (response as any).headers.get('set-cookie')?.split(';')[0];
|
||||
});
|
||||
// --- START DEBUG LOGGING ---
|
||||
// Query the DB from within the test file to see its state.
|
||||
getPool().query('SELECT u.user_id, u.email, p.role FROM public.users u JOIN public.profiles p ON u.user_id = p.user_id').then(res => {
|
||||
@@ -58,4 +66,23 @@ describe('Authentication API Integration', () => {
|
||||
// We expect the loginUser function to throw an error for a failed login.
|
||||
await expect(loginUser(adminEmail, wrongPassword, false)).rejects.toThrow('Incorrect email or password.');
|
||||
});
|
||||
|
||||
it('should successfully refresh an access token using a refresh token cookie', async () => {
|
||||
// Arrange: We have a refreshTokenCookie from the beforeAll hook.
|
||||
expect(refreshTokenCookie).toBeDefined();
|
||||
|
||||
// Act: Make a request to the refresh-token endpoint, including the cookie.
|
||||
const apiUrl = process.env.VITE_API_BASE_URL || 'http://localhost:3001/api';
|
||||
const response = await fetch(`${apiUrl}/auth/refresh-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Cookie': refreshTokenCookie!,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert: Check for a successful response and a new access token.
|
||||
expect(response.ok).toBe(true);
|
||||
const data = await response.json();
|
||||
expect(data.token).toBeTypeOf('string');
|
||||
});
|
||||
});
|
||||
55
src/tests/integration/public.integration.test.ts
Normal file
55
src/tests/integration/public.integration.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// src/tests/integration/public.integration.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
describe('Public API Routes Integration Tests', () => {
|
||||
describe('Health Check Endpoints', () => {
|
||||
it('GET /api/health/ping should return "pong"', async () => {
|
||||
const isOk = await apiClient.pingBackend();
|
||||
expect(isOk).toBe(true);
|
||||
});
|
||||
|
||||
it('GET /api/health/db-schema should return success', async () => {
|
||||
const result = await apiClient.checkDbSchema();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('All required database tables exist.');
|
||||
});
|
||||
|
||||
it('GET /api/health/storage should return success', async () => {
|
||||
// This assumes the STORAGE_PATH is correctly set up for the test environment
|
||||
const result = await apiClient.checkStorage();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('is accessible and writable');
|
||||
});
|
||||
|
||||
it('GET /api/health/db-pool should return success', async () => {
|
||||
const result = await apiClient.checkDbPoolHealth();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('Pool Status:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Public Data Endpoints', () => {
|
||||
it('GET /api/flyers should return a list of flyers', async () => {
|
||||
const flyers = await apiClient.fetchFlyers();
|
||||
expect(flyers).toBeInstanceOf(Array);
|
||||
// The seed script creates at least one flyer
|
||||
expect(flyers.length).toBeGreaterThan(0);
|
||||
expect(flyers[0]).toHaveProperty('flyer_id');
|
||||
expect(flyers[0]).toHaveProperty('store');
|
||||
});
|
||||
|
||||
it('GET /api/master-items should return a list of master items', async () => {
|
||||
const masterItems = await apiClient.fetchMasterItems();
|
||||
expect(masterItems).toBeInstanceOf(Array);
|
||||
// The seed script creates master items
|
||||
expect(masterItems.length).toBeGreaterThan(0);
|
||||
expect(masterItems[0]).toHaveProperty('master_grocery_item_id');
|
||||
expect(masterItems[0]).toHaveProperty('category_name');
|
||||
});
|
||||
});
|
||||
});
|
||||
20
src/tests/integration/system.integration.test.ts
Normal file
20
src/tests/integration/system.integration.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// src/tests/integration/system.integration.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
describe('System API Routes Integration Tests', () => {
|
||||
describe('GET /api/system/pm2-status', () => {
|
||||
it('should return a status for PM2', async () => {
|
||||
// In a typical CI environment without PM2, this will fail gracefully.
|
||||
// The test verifies that the endpoint responds correctly, even if PM2 isn't running.
|
||||
const result = await apiClient.checkPm2Status();
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveProperty('success');
|
||||
expect(result).toHaveProperty('message');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -157,4 +157,51 @@ describe('User API Routes Integration Tests', () => {
|
||||
expect(loginResponse.user).toBeDefined();
|
||||
expect(loginResponse.user.user_id).toBe(resetUser.user_id);
|
||||
});
|
||||
|
||||
describe('User Data Routes (Watched Items & Shopping Lists)', () => {
|
||||
it('should allow a user to add and remove a watched item', async () => {
|
||||
// Act 1: Add a new watched item.
|
||||
const newItem = await apiClient.addWatchedItem('Integration Test Item', 'Pantry & Dry Goods', authToken);
|
||||
|
||||
// Assert 1: Check that the item was created correctly.
|
||||
expect(newItem).toBeDefined();
|
||||
expect(newItem.name).toBe('Integration Test Item');
|
||||
|
||||
// Act 2: Fetch all watched items for the user.
|
||||
const watchedItems = await apiClient.fetchWatchedItems(authToken);
|
||||
|
||||
// Assert 2: Verify the new item is in the list.
|
||||
expect(watchedItems.some(item => item.master_grocery_item_id === newItem.master_grocery_item_id)).toBe(true);
|
||||
|
||||
// Act 3: Remove the watched item.
|
||||
await apiClient.removeWatchedItem(newItem.master_grocery_item_id, authToken);
|
||||
|
||||
// Assert 3: Fetch again and verify the item is gone.
|
||||
const finalWatchedItems = await apiClient.fetchWatchedItems(authToken);
|
||||
expect(finalWatchedItems.some(item => item.master_grocery_item_id === newItem.master_grocery_item_id)).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow a user to manage a shopping list and its items', async () => {
|
||||
// Act 1: Create a new shopping list.
|
||||
const newList = await apiClient.createShoppingList('My Integration Test List', authToken);
|
||||
|
||||
// Assert 1: Check that the list was created.
|
||||
expect(newList).toBeDefined();
|
||||
expect(newList.name).toBe('My Integration Test List');
|
||||
|
||||
// Act 2: Add an item to the new list.
|
||||
const addedItem = await apiClient.addShoppingListItem(newList.shopping_list_id, { customItemName: 'Custom Test Item' }, authToken);
|
||||
|
||||
// Assert 2: Check that the item was added.
|
||||
expect(addedItem).toBeDefined();
|
||||
expect(addedItem.custom_item_name).toBe('Custom Test Item');
|
||||
|
||||
// Act 3: Fetch the lists again to verify the item is present.
|
||||
const lists = await apiClient.fetchShoppingLists(authToken);
|
||||
const updatedList = lists.find(l => l.shopping_list_id === newList.shopping_list_id);
|
||||
expect(updatedList).toBeDefined();
|
||||
expect(updatedList?.items).toHaveLength(1);
|
||||
expect(updatedList?.items[0].shopping_list_item_id).toBe(addedItem.shopping_list_item_id);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user