diff --git a/apps/web/src/app/(portal)/marketplace/marketplace.test.tsx b/apps/web/src/app/(portal)/marketplace/marketplace.test.tsx new file mode 100644 index 0000000..7e71c37 --- /dev/null +++ b/apps/web/src/app/(portal)/marketplace/marketplace.test.tsx @@ -0,0 +1,181 @@ +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// Mock next-intl +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => { + const translations: Record = { + title: 'Marktplatz', + subtitle: 'Entdecken und aktivieren Sie Module fuer Ihren Mandanten', + activate: 'Modul aktivieren', + deactivate: 'Deaktivieren', + statusActive: 'Aktiviert', + statusAvailable: 'Verfuegbar', + emptyTitle: 'Noch keine Module verfuegbar', + emptyBody: 'Module werden vom Systemadministrator bereitgestellt.', + accessDenied: 'Zugriff verweigert', + }; + return translations[key] ?? key; + }, +})); + +// Mock auth store +const mockAuthStore = vi.fn(); +vi.mock('@/lib/stores/auth-store', () => ({ + useAuthStore: (selector: (state: unknown) => unknown) => mockAuthStore(selector), +})); + +// Mock marketplace store +vi.mock('@/lib/stores/marketplace-store', () => ({ + useMarketplaceStore: () => ({ + sidebarRefreshKey: 0, + bumpSidebarRefresh: vi.fn(), + selectedTenantId: null, + setSelectedTenantId: vi.fn(), + }), +})); + +const mockModules = [ + { + id: 'mod-1', + slug: 'domaincheck', + name: 'Domaincheck', + version: '1.0.0', + category: 'Domain-Tools', + description: { de: 'Domain pruefen', en: 'Check domain' }, + icon: 'Globe', + isSystem: false, + }, + { + id: 'mod-2', + slug: 'converter', + name: 'Converter', + version: '1.0.0', + category: 'Utilities', + description: { de: 'Dateien konvertieren', en: 'Convert files' }, + icon: null, + isSystem: false, + }, +]; + +const mockActiveModules = [ + { + id: 'mod-1', + slug: 'domaincheck', + name: 'Domaincheck', + version: '1.0.0', + category: 'Domain-Tools', + description: { de: 'Domain pruefen', en: 'Check domain' }, + icon: 'Globe', + isSystem: false, + }, +]; + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +// Lazy import after mocks +async function importPage() { + const mod = await import('./page'); + return mod.default; +} + +describe('MarketplacePage', () => { + beforeEach(() => { + // Default: admin user + mockAuthStore.mockImplementation((selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) => + selector({ + user: { id: 'u1', username: 'admin', displayName: 'Admin', role: 'ADMIN', tenantId: 't1' }, + }), + ); + }); + + it('renders one MarketplaceCard per module when fetch returns a list', async () => { + vi.stubGlobal( + 'fetch', + vi.fn((url: string) => { + if (url.includes('/modules/active')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); + } + if (url.includes('/modules')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockModules) }); + } + return Promise.resolve({ ok: false, text: () => Promise.resolve('') }); + }), + ); + + const Page = await importPage(); + render(); + + await waitFor(() => { + expect(screen.getByText('Domaincheck')).toBeInTheDocument(); + }); + expect(screen.getByText('Converter')).toBeInTheDocument(); + }); + + it('shows activated status for a module returned by /modules/active', async () => { + vi.stubGlobal( + 'fetch', + vi.fn((url: string) => { + if (url.includes('/modules/active')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockActiveModules) }); + } + if (url.includes('/modules')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockModules) }); + } + return Promise.resolve({ ok: false, text: () => Promise.resolve('') }); + }), + ); + + const Page = await importPage(); + render(); + + await waitFor(() => { + expect(screen.getByText('Domaincheck')).toBeInTheDocument(); + }); + + // One card should be activated, one available + const statusBadges = screen.getAllByRole('status'); + const texts = statusBadges.map((b) => b.textContent); + expect(texts).toContain('Aktiviert'); + expect(texts).toContain('Verfuegbar'); + }); + + it('renders access-denied and no cards for non-admin users', async () => { + mockAuthStore.mockImplementation((selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) => + selector({ + user: { id: 'u2', username: 'user', displayName: 'User', role: 'USER', tenantId: 't1' }, + }), + ); + + const Page = await importPage(); + render(); + + expect(screen.getByText('Zugriff verweigert')).toBeInTheDocument(); + expect(screen.queryByText('Domaincheck')).not.toBeInTheDocument(); + }); + + it('renders empty-state heading when no modules are available', async () => { + vi.stubGlobal( + 'fetch', + vi.fn((url: string) => { + if (url.includes('/modules/active')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); + } + if (url.includes('/modules')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); + } + return Promise.resolve({ ok: false, text: () => Promise.resolve('') }); + }), + ); + + const Page = await importPage(); + render(); + + await waitFor(() => { + expect(screen.getByText('Noch keine Module verfuegbar')).toBeInTheDocument(); + }); + }); +});