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

This commit is contained in:
2025-11-26 19:47:13 -08:00
parent e662dc1b30
commit 9c77f0599c
7 changed files with 31 additions and 69 deletions

View File

@@ -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

View File

@@ -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.';

View File

@@ -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.

View File

@@ -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[];

View File

@@ -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' }) }));
});
});

View File

@@ -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);

View File

@@ -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 {