/** * Integration Tests * Tests for complete user flows and component interactions * Covers: registration → login → dashboard, portfolio creation → upload → deploy */ import React from 'react' import { renderWithProviders, userEvent, screen, waitFor } from '@/__tests__/utils/test-helpers' import { useAuth } from '@/hooks/use-auth' import { usePortfolios } from '@/hooks/use-portfolios' jest.mock('@/hooks/use-auth') jest.mock('@/hooks/use-portfolios') jest.mock('@/components/ui/button', () => ({ Button: ({ children, ...props }: any) => , })) jest.mock('@/components/ui/input', () => ({ Input: (props: any) => , })) jest.mock('next/link', () => ({ __esModule: true, default: ({ children, href }: any) => {children}, })) describe('Integration - User Registration Flow', () => { const mockRegister = jest.fn() const mockLoginAfterRegister = jest.fn() beforeEach(() => { jest.clearAllMocks() ;(useAuth as jest.Mock).mockReturnValue({ register: mockRegister, login: mockLoginAfterRegister, user: null, isLoading: false, }) }) it('should complete full registration flow', async () => { mockRegister.mockResolvedValueOnce({ id: 1, name: 'John Doe', email: 'john@example.com', created_at: new Date().toISOString(), }) // Simulate registration form submission const registrationData = { name: 'John Doe', email: 'john@example.com', password: 'Password123!', password_confirmation: 'Password123!', } // User should be able to complete registration expect(mockRegister).not.toHaveBeenCalled() // Simulate form submission await mockRegister(registrationData) expect(mockRegister).toHaveBeenCalledWith(registrationData) }) it('should prevent registration with existing email', async () => { const errorMessage = 'Email already registered' mockRegister.mockRejectedValueOnce(new Error(errorMessage)) try { await mockRegister({ name: 'Jane Doe', email: 'existing@example.com', password: 'Password123!', password_confirmation: 'Password123!', }) } catch (error: any) { expect(error.message).toBe(errorMessage) } expect(mockRegister).toHaveBeenCalled() }) it('should validate all fields before submission', () => { const incompleteData = { name: '', email: 'john@example.com', password: 'Password123!', password_confirmation: 'Password123!', } // Validation should fail for empty name expect(incompleteData.name).toBe('') }) }) describe('Integration - User Login Flow', () => { const mockLogin = jest.fn() const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com', created_at: '', updated_at: '' } beforeEach(() => { jest.clearAllMocks() ;(useAuth as jest.Mock).mockReturnValue({ login: mockLogin, user: null, isLoading: false, }) }) it('should complete full login flow', async () => { mockLogin.mockResolvedValueOnce(mockUser) const loginData = { email: 'john@example.com', password: 'Password123!', remember: false, } await mockLogin(loginData) expect(mockLogin).toHaveBeenCalledWith(loginData) }) it('should handle invalid credentials', async () => { mockLogin.mockRejectedValueOnce(new Error('Invalid email or password')) try { await mockLogin({ email: 'wrong@example.com', password: 'WrongPassword', }) } catch (error: any) { expect(error.message).toBe('Invalid email or password') } }) it('should handle 401 unauthorized response', async () => { mockLogin.mockRejectedValueOnce(new Error('401 Unauthorized')) try { await mockLogin({ email: 'john@example.com', password: 'wrong-password', }) } catch (error: any) { expect(error.message).toBe('401 Unauthorized') } }) }) describe('Integration - Portfolio Creation Flow', () => { const mockCreatePortfolio = jest.fn() const mockUploadPortfolio = jest.fn() beforeEach(() => { jest.clearAllMocks() ;(usePortfolios as jest.Mock).mockReturnValue({ portfolios: [], isLoading: false, error: null, createPortfolio: mockCreatePortfolio, uploadPortfolio: mockUploadPortfolio, deployPortfolio: jest.fn(), }) }) it('should create portfolio and upload file in sequence', async () => { mockCreatePortfolio.mockResolvedValueOnce({ id: 1, name: 'My Portfolio', domain: 'myportfolio.com', path: null, active: false, }) mockUploadPortfolio.mockResolvedValueOnce({ id: 1, name: 'My Portfolio', domain: 'myportfolio.com', path: '/uploads/1.zip', active: false, }) // Step 1: Create portfolio await mockCreatePortfolio('My Portfolio', 'myportfolio.com') expect(mockCreatePortfolio).toHaveBeenCalledWith('My Portfolio', 'myportfolio.com') // Step 2: Upload file to portfolio const file = new File(['test'], 'portfolio.zip', { type: 'application/zip' }) await mockUploadPortfolio(1, file) expect(mockUploadPortfolio).toHaveBeenCalledWith(1, file) }) it('should handle portfolio creation validation errors', async () => { mockCreatePortfolio.mockRejectedValueOnce( new Error('Portfolio name is required') ) try { await mockCreatePortfolio('', 'domain.com') } catch (error: any) { expect(error.message).toBe('Portfolio name is required') } expect(mockCreatePortfolio).toHaveBeenCalled() }) it('should handle domain validation errors', async () => { mockCreatePortfolio.mockRejectedValueOnce( new Error('Invalid domain format') ) try { await mockCreatePortfolio('Portfolio', 'invalid-domain') } catch (error: any) { expect(error.message).toBe('Invalid domain format') } }) it('should prevent upload without portfolio creation', async () => { const file = new File(['test'], 'portfolio.zip', { type: 'application/zip' }) // Should not allow upload to non-existent portfolio expect(() => { mockUploadPortfolio(999, file) // Portfolio ID that doesn't exist }).not.toThrow() }) }) describe('Integration - Portfolio Deployment Flow', () => { const mockDeployPortfolio = jest.fn() const mockUploadPortfolio = jest.fn() beforeEach(() => { jest.clearAllMocks() ;(usePortfolios as jest.Mock).mockReturnValue({ portfolios: [], isLoading: false, error: null, createPortfolio: jest.fn(), uploadPortfolio: mockUploadPortfolio, deployPortfolio: mockDeployPortfolio, }) }) it('should deploy portfolio after successful upload', async () => { mockUploadPortfolio.mockResolvedValueOnce({ id: 1, name: 'My Portfolio', domain: 'myportfolio.com', path: '/uploads/1.zip', active: false, }) mockDeployPortfolio.mockResolvedValueOnce({ id: 1, name: 'My Portfolio', domain: 'myportfolio.com', path: '/uploads/1.zip', active: true, }) // Step 1: Upload file const file = new File(['test'], 'portfolio.zip', { type: 'application/zip' }) await mockUploadPortfolio(1, file) // Step 2: Deploy portfolio await mockDeployPortfolio(1) expect(mockDeployPortfolio).toHaveBeenCalledWith(1) }) it('should handle deployment failure', async () => { mockDeployPortfolio.mockRejectedValueOnce( new Error('Deployment failed: Server error') ) try { await mockDeployPortfolio(1) } catch (error: any) { expect(error.message).toBe('Deployment failed: Server error') } }) it('should prevent deployment of portfolio without uploaded file', () => { // Portfolio without path (no uploaded file) should not deploy const portfolioWithoutFile = { id: 1, name: 'Portfolio', domain: 'portfolio.com', path: null, active: false, } expect(portfolioWithoutFile.path).toBeNull() }) }) describe('Integration - Complete User Journey', () => { const mockRegister = jest.fn() const mockLogin = jest.fn() const mockCreatePortfolio = jest.fn() const mockUploadPortfolio = jest.fn() const mockDeployPortfolio = jest.fn() const mockLogout = jest.fn() beforeEach(() => { jest.clearAllMocks() ;(useAuth as jest.Mock).mockReturnValue({ register: mockRegister, login: mockLogin, logout: mockLogout, user: null, isLoading: false, }) ;(usePortfolios as jest.Mock).mockReturnValue({ portfolios: [], isLoading: false, error: null, createPortfolio: mockCreatePortfolio, uploadPortfolio: mockUploadPortfolio, deployPortfolio: mockDeployPortfolio, }) }) it('should complete full user journey: register → login → create → upload → deploy', async () => { // Step 1: Registration const newUser = { id: 1, name: 'John Doe', email: 'john@example.com', created_at: new Date().toISOString(), } mockRegister.mockResolvedValueOnce(newUser) await mockRegister({ name: 'John Doe', email: 'john@example.com', password: 'SecurePass123!', password_confirmation: 'SecurePass123!', }) expect(mockRegister).toHaveBeenCalled() // Step 2: Login (after registration or on new session) mockLogin.mockResolvedValueOnce(newUser) await mockLogin({ email: 'john@example.com', password: 'SecurePass123!', }) expect(mockLogin).toHaveBeenCalled() // Step 3: Create Portfolio const portfolio = { id: 1, name: 'My Digital Portfolio', domain: 'johndoe.com', path: null, active: false, } mockCreatePortfolio.mockResolvedValueOnce(portfolio) await mockCreatePortfolio('My Digital Portfolio', 'johndoe.com') expect(mockCreatePortfolio).toHaveBeenCalledWith('My Digital Portfolio', 'johndoe.com') // Step 4: Upload Portfolio File const file = new File(['portfolio content'], 'portfolio.zip', { type: 'application/zip' }) const uploadedPortfolio = { ...portfolio, path: '/uploads/1.zip' } mockUploadPortfolio.mockResolvedValueOnce(uploadedPortfolio) await mockUploadPortfolio(1, file) expect(mockUploadPortfolio).toHaveBeenCalledWith(1, file) // Step 5: Deploy Portfolio const deployedPortfolio = { ...uploadedPortfolio, active: true } mockDeployPortfolio.mockResolvedValueOnce(deployedPortfolio) await mockDeployPortfolio(1) expect(mockDeployPortfolio).toHaveBeenCalledWith(1) // Step 6: Logout mockLogout.mockResolvedValueOnce(undefined) await mockLogout() expect(mockLogout).toHaveBeenCalled() }) it('should handle error recovery during portfolio creation', async () => { // Initial failed attempt mockCreatePortfolio.mockRejectedValueOnce(new Error('Network error')) try { await mockCreatePortfolio('Portfolio', 'domain.com') } catch (error: any) { expect(error.message).toBe('Network error') } // Retry successful mockCreatePortfolio.mockResolvedValueOnce({ id: 1, name: 'Portfolio', domain: 'domain.com', path: null, active: false, }) await mockCreatePortfolio('Portfolio', 'domain.com') expect(mockCreatePortfolio).toHaveBeenCalledTimes(2) }) it('should handle concurrent portfolio operations', async () => { const portfolio1 = { id: 1, name: 'Portfolio 1', domain: 'portfolio1.com', path: null, active: false, } const portfolio2 = { id: 2, name: 'Portfolio 2', domain: 'portfolio2.com', path: null, active: false, } mockCreatePortfolio.mockResolvedValueOnce(portfolio1) mockCreatePortfolio.mockResolvedValueOnce(portfolio2) // Create both portfolios await Promise.all([ mockCreatePortfolio('Portfolio 1', 'portfolio1.com'), mockCreatePortfolio('Portfolio 2', 'portfolio2.com'), ]) expect(mockCreatePortfolio).toHaveBeenCalledTimes(2) }) }) describe('Integration - Error Recovery Scenarios', () => { const mockLogin = jest.fn() const mockUploadPortfolio = jest.fn() beforeEach(() => { jest.clearAllMocks() ;(useAuth as jest.Mock).mockReturnValue({ login: mockLogin, user: null, isLoading: false, }) ;(usePortfolios as jest.Mock).mockReturnValue({ portfolios: [], isLoading: false, error: null, uploadPortfolio: mockUploadPortfolio, createPortfolio: jest.fn(), deployPortfolio: jest.fn(), }) }) it('should recover from network timeout during login', async () => { // First attempt times out mockLogin.mockRejectedValueOnce(new Error('Connection timeout')) try { await mockLogin({ email: 'user@example.com', password: 'pass' }) } catch (error: any) { expect(error.message).toBe('Connection timeout') } // User retries and succeeds mockLogin.mockResolvedValueOnce({ id: 1, name: 'User', email: 'user@example.com', created_at: '', updated_at: '' }) await mockLogin({ email: 'user@example.com', password: 'pass' }) expect(mockLogin).toHaveBeenCalledTimes(2) }) it('should recover from file upload failure', async () => { const file = new File(['content'], 'portfolio.zip', { type: 'application/zip' }) // First attempt fails mockUploadPortfolio.mockRejectedValueOnce(new Error('Upload failed')) try { await mockUploadPortfolio(1, file) } catch (error: any) { expect(error.message).toBe('Upload failed') } // Retry succeeds mockUploadPortfolio.mockResolvedValueOnce({ id: 1, name: 'Portfolio', domain: 'portfolio.com', path: '/uploads/1.zip', active: false, }) await mockUploadPortfolio(1, file) expect(mockUploadPortfolio).toHaveBeenCalledTimes(2) }) it('should handle session expiration and re-authentication', async () => { // Simulate session expired error mockLogin.mockRejectedValueOnce(new Error('401 Unauthorized: Session expired')) try { await mockLogin({ email: 'user@example.com', password: 'pass' }) } catch (error: any) { expect(error.message).toContain('401 Unauthorized') } // User logs in again mockLogin.mockResolvedValueOnce({ id: 1, name: 'User', email: 'user@example.com', created_at: '', updated_at: '' }) await mockLogin({ email: 'user@example.com', password: 'pass' }) expect(mockLogin).toHaveBeenCalledTimes(2) }) it('should handle API validation errors with user guidance', async () => { mockUploadPortfolio.mockRejectedValueOnce( new Error('File must be a valid ZIP archive') ) try { const invalidFile = new File(['not a zip'], 'file.txt', { type: 'text/plain' }) await mockUploadPortfolio(1, invalidFile) } catch (error: any) { expect(error.message).toContain('ZIP') } }) }) describe('Integration - State Consistency', () => { const mockCreatePortfolio = jest.fn() const mockDeployPortfolio = jest.fn() beforeEach(() => { jest.clearAllMocks() ;(usePortfolios as jest.Mock).mockReturnValue({ portfolios: [], isLoading: false, error: null, createPortfolio: mockCreatePortfolio, uploadPortfolio: jest.fn(), deployPortfolio: mockDeployPortfolio, }) }) it('should maintain consistent state after operations', async () => { const portfolio = { id: 1, name: 'Portfolio', domain: 'portfolio.com', path: null, active: false, } mockCreatePortfolio.mockResolvedValueOnce(portfolio) const created = await mockCreatePortfolio('Portfolio', 'portfolio.com') // Verify state consistency expect(created.id).toBe(1) expect(created.active).toBe(false) expect(created.path).toBeNull() // After deployment mockDeployPortfolio.mockResolvedValueOnce({ ...portfolio, active: true }) const deployed = await mockDeployPortfolio(1) // Verify deployment changed state expect(deployed.active).toBe(true) expect(deployed.id).toBe(portfolio.id) }) it('should prevent state mutations during async operations', async () => { const portfolio = { id: 1, name: 'Portfolio', domain: 'portfolio.com', path: null, active: false, } mockCreatePortfolio.mockImplementationOnce( () => new Promise((resolve) => { setTimeout(() => resolve(portfolio), 50) }) ) const promise = mockCreatePortfolio('Portfolio', 'portfolio.com') // Portfolio should not be mutated expect(portfolio.active).toBe(false) await promise }) })