we went to mocks - now going to unit-setup.ts - centralized
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 37m52s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 37m52s
This commit is contained in:
@@ -7,7 +7,7 @@ import pluginReactRefresh from "eslint-plugin-react-refresh";
|
||||
export default tseslint.config(
|
||||
{
|
||||
// Global ignores
|
||||
ignores: ["dist", ".gitea", "supabase", "node_modules", "*.cjs"],
|
||||
ignores: ["dist", ".gitea", "node_modules", "*.cjs"],
|
||||
},
|
||||
{
|
||||
// All files
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
-- ============================================================================
|
||||
-- PART 2: TABLES
|
||||
-- ============================================================================
|
||||
-- 1. Users - This replaces the Supabase `auth.users` table.
|
||||
-- 1. Users table for authentication.
|
||||
CREATE TABLE IF NOT EXISTS public.users (
|
||||
user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
@@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS public.users (
|
||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||
);
|
||||
COMMENT ON TABLE public.users IS 'Stores user authentication information, replacing Supabase auth.';
|
||||
COMMENT ON TABLE public.users IS 'Stores user authentication information.';
|
||||
COMMENT ON COLUMN public.users.refresh_token IS 'Stores the long-lived refresh token for re-authentication.';
|
||||
COMMENT ON COLUMN public.users.failed_login_attempts IS 'Tracks the number of consecutive failed login attempts.';
|
||||
COMMENT ON COLUMN public.users.last_failed_login IS 'Timestamp of the last failed login attempt.';
|
||||
@@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS public.flyers (
|
||||
COMMENT ON TABLE public.flyers IS 'Stores metadata for each processed flyer, linking it to a store and its validity period.';
|
||||
CREATE INDEX IF NOT EXISTS idx_flyers_store_id ON public.flyers(store_id);
|
||||
COMMENT ON COLUMN public.flyers.file_name IS 'The original name of the uploaded flyer file (e.g., "flyer_week_1.pdf").';
|
||||
COMMENT ON COLUMN public.flyers.image_url IS 'The public URL of the primary flyer image stored in Supabase Storage.';
|
||||
COMMENT ON COLUMN public.flyers.image_url IS 'The public URL of the primary flyer image stored on Server for now.';
|
||||
COMMENT ON COLUMN public.flyers.checksum IS 'A SHA-256 hash of the original file content to prevent duplicate processing.';
|
||||
COMMENT ON COLUMN public.flyers.store_id IS 'Foreign key linking this flyer to a specific store in the `stores` table.';
|
||||
COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
-- MASTER SCHEMA SCRIPT
|
||||
-- ============================================================================
|
||||
-- Purpose:
|
||||
-- This file contains the master SQL schema for the entire Supabase database.
|
||||
-- This file contains the master SQL schema for the entire Postgres database.
|
||||
-- It is designed to be a "one-click" script that can be run in a PostgreSQL
|
||||
-- database to set up the entire backend from scratch, including:
|
||||
-- 1. Enabling required Postgres extensions.
|
||||
@@ -19,7 +19,7 @@
|
||||
-- ============================================================================
|
||||
-- PART 2: TABLES
|
||||
-- ============================================================================
|
||||
-- 1. Users - This replaces the Supabase `auth.users` table.
|
||||
-- 1. Users table for authentication.
|
||||
CREATE TABLE IF NOT EXISTS public.users (
|
||||
user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
@@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS public.users (
|
||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||
);
|
||||
COMMENT ON TABLE public.users IS 'Stores user authentication information, replacing Supabase auth.';
|
||||
COMMENT ON TABLE public.users IS 'Stores user authentication information.';
|
||||
COMMENT ON COLUMN public.users.refresh_token IS 'Stores the long-lived refresh token for re-authentication.';
|
||||
COMMENT ON COLUMN public.users.failed_login_attempts IS 'Tracks the number of consecutive failed login attempts.';
|
||||
COMMENT ON COLUMN public.users.last_failed_login IS 'Timestamp of the last failed login attempt.';
|
||||
@@ -107,7 +107,7 @@ CREATE INDEX IF NOT EXISTS idx_flyers_store_id ON public.flyers(store_id);
|
||||
-- This is particularly useful for the recalculate_price_history_on_flyer_item_delete trigger.
|
||||
CREATE INDEX IF NOT EXISTS idx_flyers_valid_dates_store ON public.flyers(valid_from, valid_to, store_id);
|
||||
COMMENT ON COLUMN public.flyers.file_name IS 'The original name of the uploaded flyer file (e.g., "flyer_week_1.pdf").';
|
||||
COMMENT ON COLUMN public.flyers.image_url IS 'The public URL of the primary flyer image stored in Supabase Storage.';
|
||||
COMMENT ON COLUMN public.flyers.image_url IS 'The public URL of the primary flyer image stored on server Storage.';
|
||||
COMMENT ON COLUMN public.flyers.checksum IS 'A SHA-256 hash of the original file content to prevent duplicate processing.';
|
||||
COMMENT ON COLUMN public.flyers.store_id IS 'Foreign key linking this flyer to a specific store in the `stores` table.';
|
||||
COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.';
|
||||
@@ -1066,26 +1066,8 @@ INSERT INTO public.dietary_restrictions (name, type) VALUES
|
||||
('Tree Nuts', 'allergy'), ('Peanuts', 'allergy'), ('Soy', 'allergy'), ('Wheat', 'allergy')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 3: STORAGE
|
||||
-- ============================================================================
|
||||
-- The `storage.buckets` table is a Supabase-specific feature.
|
||||
-- This section is removed. You will need to implement your own file storage
|
||||
-- solution (e.g., local filesystem, S3-compatible service) and store
|
||||
-- URLs/paths in the `flyers.image_url` column.
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 4: ROW LEVEL SECURITY (RLS)
|
||||
-- ============================================================================
|
||||
-- Row Level Security (RLS) policies are removed as they rely on Supabase-specific
|
||||
-- functions like `auth.uid()`. In a self-hosted environment, authorization
|
||||
-- should be handled at the application layer (e.g., your backend API ensures
|
||||
-- a user can only query their own data).
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 5: DATABASE FUNCTIONS
|
||||
-- PART 3: DATABASE FUNCTIONS
|
||||
-- ============================================================================
|
||||
-- Function to find the best current sale price for a user's watched items.
|
||||
-- This function queries all currently active flyers to find the lowest price
|
||||
@@ -2007,11 +1989,8 @@ AS $$
|
||||
$$;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 7: TRIGGERS
|
||||
-- PART 4: TRIGGERS
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. Set up the trigger to automatically create a profile when a new user signs up.
|
||||
|
||||
11
src/App.tsx
11
src/App.tsx
@@ -1,6 +1,6 @@
|
||||
// src/App.tsx
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Routes, Route } from 'react-router-dom'; // This was a duplicate, fixed.
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { FlyerDisplay } from './features/flyer/FlyerDisplay';
|
||||
import { ExtractedDataTable } from './features/flyer/ExtractedDataTable';
|
||||
@@ -9,12 +9,12 @@ import { PriceChart } from './features/charts/PriceChart';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { ErrorDisplay } from './components/ErrorDisplay';
|
||||
import { Header } from './components/Header';
|
||||
import { logger } from './services/logger'; // This is correct
|
||||
import { logger } from './services/logger';
|
||||
import * as aiApiClient from './services/aiApiClient';
|
||||
import type { FlyerItem, Flyer, MasterGroceryItem, DealItem, ProcessingStage, StageStatus, Profile, ShoppingList, ShoppingListItem, User, UserProfile } from './types';
|
||||
import { BulkImporter } from './features/flyer/BulkImporter';
|
||||
import { PriceHistoryChart } from './features/charts/PriceHistoryChart'; // This import seems to have a supabase dependency, but the component is not provided. Assuming it will be updated separately.
|
||||
import * as apiClient from './services/apiClient'; // updateUserPreferences is no longer called directly from App.tsx
|
||||
import { PriceHistoryChart } from './features/charts/PriceHistoryChart';
|
||||
import * as apiClient from './services/apiClient';
|
||||
import { FlyerList } from './features/flyer/FlyerList';
|
||||
import { recordProcessingTime, getAverageProcessingTime } from './utils/processingTimer';
|
||||
import { ProcessingStatus } from './features/flyer/ProcessingStatus';
|
||||
@@ -33,7 +33,7 @@ import { WatchedItemsList } from './features/shopping/WatchedItemsList';
|
||||
import { AdminStatsPage } from './pages/admin/AdminStatsPage';
|
||||
import { ResetPasswordPage } from './pages/ResetPasswordPage';
|
||||
import { AnonymousUserBanner } from './pages/admin/components/AnonymousUserBanner';
|
||||
import { VoiceLabPage } from './pages/VoiceLabPage'; // Import the new page
|
||||
import { VoiceLabPage } from './pages/VoiceLabPage';
|
||||
import { WhatsNewModal } from './components/WhatsNewModal';
|
||||
import { QuestionMarkCircleIcon } from './components/icons/QuestionMarkCircleIcon';
|
||||
|
||||
@@ -99,6 +99,7 @@ function App() {
|
||||
const [processingProgress, setProcessingProgress] = useState(0);
|
||||
const [currentFile, setCurrentFile] = useState<string | null>(null);
|
||||
const [fileCount, setFileCount] = useState<{current: number, total: number} | null>(null);
|
||||
|
||||
const [importSummary, setImportSummary] = useState<{
|
||||
processed: string[];
|
||||
skipped: string[];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// src/components/ProfileManager.test.tsx
|
||||
// src/pages/admin/ProfileManager.test.tsx
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
|
||||
@@ -313,25 +313,6 @@ describe('ProfileManager Authentication Flows', () => {
|
||||
Object.defineProperty(window, 'location', { writable: true, value: originalLocation });
|
||||
});
|
||||
|
||||
it('should trigger data export', async () => {
|
||||
// To test the download functionality without interfering with React's rendering,
|
||||
// we spy on the `click` method of the anchor element's prototype.
|
||||
// This is safer than mocking `document.createElement`.
|
||||
const anchorClickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {
|
||||
// We provide an empty implementation to prevent the test environment from trying to navigate.
|
||||
});
|
||||
|
||||
render(<ProfileManager {...authenticatedProps} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /data & privacy/i }));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /export my data/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApiClient.exportUserData).toHaveBeenCalled();
|
||||
expect(anchorClickSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should attempt to redirect when GitHub OAuth button is clicked', () => {
|
||||
const originalLocation = window.location;
|
||||
const mockLocation = { href: '' };
|
||||
@@ -373,7 +354,7 @@ describe('ProfileManager Authenticated User Features', () => {
|
||||
// Mock successful API calls by default
|
||||
(mockedApiClient.updateUserProfile as Mock).mockImplementation((data) => Promise.resolve({ ok: true, json: () => Promise.resolve({ ...authenticatedProfile, ...data as object }) } as Response));
|
||||
(mockedApiClient.updateUserPassword as Mock).mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: 'Password updated successfully.' }))
|
||||
new Response(JSON.stringify({ message: 'Password updated successfully.' }), { status: 200 })
|
||||
);
|
||||
(mockedApiClient.updateUserPreferences as Mock).mockImplementation((prefs) => Promise.resolve({ ok: true, json: () => Promise.resolve({ ...authenticatedProfile, preferences: { ...authenticatedProfile.preferences, ...prefs } }) } as Response));
|
||||
(mockedApiClient.exportUserData as Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve({ profile: authenticatedProfile, watchedItems: [], shoppingLists: [] }) } as Response);
|
||||
@@ -402,7 +383,7 @@ describe('ProfileManager Authenticated User Features', () => {
|
||||
expect(mockedApiClient.updateUserProfile).toHaveBeenCalledWith({
|
||||
full_name: 'Updated Name',
|
||||
avatar_url: 'http://example.com/avatar.png',
|
||||
}, undefined); // Assuming no token is passed directly in this mock setup
|
||||
});
|
||||
expect(mockOnProfileUpdate).toHaveBeenCalledWith(expect.objectContaining({ full_name: 'Updated Name' }));
|
||||
expect(notifySuccess).toHaveBeenCalledWith('Profile updated successfully!');
|
||||
});
|
||||
@@ -440,7 +421,7 @@ describe('ProfileManager Authenticated User Features', () => {
|
||||
fireEvent.submit(screen.getByTestId('update-password-form'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApiClient.updateUserPassword).toHaveBeenCalledWith('newpassword123', undefined);
|
||||
expect(mockedApiClient.updateUserPassword).toHaveBeenCalledWith('newpassword123');
|
||||
expect(notifySuccess).toHaveBeenCalledWith('Password updated successfully!');
|
||||
});
|
||||
});
|
||||
@@ -501,7 +482,7 @@ describe('ProfileManager Authenticated User Features', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /yes, delete my account/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApiClient.deleteUserAccount).toHaveBeenCalledWith('correctpassword', undefined);
|
||||
expect(mockedApiClient.deleteUserAccount).toHaveBeenCalledWith('correctpassword');
|
||||
expect(notifySuccess).toHaveBeenCalledWith("Account deleted successfully. You will be logged out shortly.");
|
||||
});
|
||||
|
||||
@@ -548,7 +529,7 @@ describe('ProfileManager Authenticated User Features', () => {
|
||||
fireEvent.click(darkModeToggle);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApiClient.updateUserPreferences).toHaveBeenCalledWith({ darkMode: true }, undefined);
|
||||
expect(mockedApiClient.updateUserPreferences).toHaveBeenCalledWith({ darkMode: true });
|
||||
expect(mockOnProfileUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ preferences: expect.objectContaining({ darkMode: true }) })
|
||||
);
|
||||
@@ -568,7 +549,7 @@ describe('ProfileManager Authenticated User Features', () => {
|
||||
fireEvent.click(metricRadio);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApiClient.updateUserPreferences).toHaveBeenCalledWith({ unitSystem: 'metric' }, undefined);
|
||||
expect(mockedApiClient.updateUserPreferences).toHaveBeenCalledWith({ unitSystem: 'metric' });
|
||||
expect(mockOnProfileUpdate).toHaveBeenCalledWith(expect.objectContaining({ preferences: expect.objectContaining({ unitSystem: 'metric' }) }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,8 @@ describe('SystemCheck', () => {
|
||||
mockedApiClient.checkStorage.mockResolvedValue(new Response(JSON.stringify({ success: true, message: 'Storage OK' })));
|
||||
mockedApiClient.checkDbPoolHealth.mockResolvedValue(new Response(JSON.stringify({ success: true, message: 'DB Pool OK' })));
|
||||
mockedApiClient.checkPm2Status.mockResolvedValue(new Response(JSON.stringify({ success: true, message: 'PM2 OK' })));
|
||||
// Add the missing mock for checkDbSchema to prevent test timeouts.
|
||||
mockedApiClient.checkDbSchema.mockResolvedValue(new Response(JSON.stringify({ success: true, message: 'Schema OK' })));
|
||||
mockedApiClient.loginUser.mockResolvedValue({ ok: true, json: () => Promise.resolve({}) } as Response); // Mock successful admin login
|
||||
|
||||
// Reset VITE_API_KEY for each test
|
||||
@@ -180,11 +182,12 @@ describe('SystemCheck', () => {
|
||||
// This is more reliable than waiting for a specific check.
|
||||
await screen.findByText(/finished in/i);
|
||||
|
||||
// Reset mocks for the re-run
|
||||
mockedApiClient.checkStorage.mockResolvedValueOnce(new Response(JSON.stringify({ success: true, message: 'Storage OK (re-run)' })));
|
||||
mockedApiClient.checkDbPoolHealth.mockResolvedValueOnce(new Response(JSON.stringify({ success: true, message: 'DB Pool OK (re-run)' })));
|
||||
mockedApiClient.checkPm2Status.mockResolvedValueOnce(new Response(JSON.stringify({ success: true, message: 'PM2 OK (re-run)' })));
|
||||
mockedApiClient.loginUser.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) } as Response);
|
||||
// For the re-run, explicitly override the default mocks from `beforeEach`
|
||||
// with new persistent mocks that return the "re-run" message.
|
||||
mockedApiClient.checkDbSchema.mockResolvedValue(new Response(JSON.stringify({ success: true, message: 'Schema OK (re-run)' })));
|
||||
mockedApiClient.checkStorage.mockResolvedValue(new Response(JSON.stringify({ success: true, message: 'Storage OK (re-run)' })));
|
||||
mockedApiClient.checkDbPoolHealth.mockResolvedValue(new Response(JSON.stringify({ success: true, message: 'DB Pool OK (re-run)' })));
|
||||
mockedApiClient.checkPm2Status.mockResolvedValue(new Response(JSON.stringify({ success: true, message: 'PM2 OK (re-run)' })));
|
||||
|
||||
const rerunButton = screen.getByRole('button', { name: /re-run checks/i });
|
||||
fireEvent.click(rerunButton);
|
||||
|
||||
@@ -111,7 +111,6 @@ passport.use(new LocalStrategy(
|
||||
// // User exists, proceed to log them in.
|
||||
// logger.info(`Google OAuth successful for existing user: ${email}`);
|
||||
// // The password_hash is intentionally destructured and discarded for security.
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// const { password_hash, ...userWithoutHash } = user;
|
||||
// return done(null, userWithoutHash);
|
||||
// } else {
|
||||
@@ -164,7 +163,6 @@ passport.use(new LocalStrategy(
|
||||
// // User exists, proceed to log them in.
|
||||
// logger.info(`GitHub OAuth successful for existing user: ${email}`);
|
||||
// // The password_hash is intentionally destructured and discarded for security.
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// const { password_hash, ...userWithoutHash } = user;
|
||||
// return done(null, userWithoutHash);
|
||||
// } else {
|
||||
|
||||
Reference in New Issue
Block a user