@wopr-network/platform-ui-core 1.27.7 → 1.27.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/next.config.ts +1 -2
- package/package.json +17 -17
- package/src/__tests__/account-switcher.test.tsx +21 -20
- package/src/__tests__/activity-page.test.tsx +2 -6
- package/src/__tests__/add-payment-method-dialog.test.tsx +9 -32
- package/src/__tests__/admin-api.test.ts +1 -6
- package/src/__tests__/admin-gpu-api.test.ts +1 -3
- package/src/__tests__/admin-marketplace-api.test.ts +1 -4
- package/src/__tests__/admin-middleware.test.ts +76 -83
- package/src/__tests__/affiliate-dashboard.test.tsx +3 -3
- package/src/__tests__/api-401-redirect.test.ts +46 -9
- package/src/__tests__/api-client.test.ts +3 -5
- package/src/__tests__/api-config.test.ts +22 -42
- package/src/__tests__/api-fleet-resources.test.ts +1 -2
- package/src/__tests__/api-fleet-trpc.test.ts +2 -8
- package/src/__tests__/api-null-guards.test.ts +3 -1
- package/src/__tests__/audit-log-table-pagination.test.tsx +2 -6
- package/src/__tests__/auth-password-reset.test.tsx +7 -21
- package/src/__tests__/auth-redirect.test.tsx +8 -2
- package/src/__tests__/auth.test.tsx +25 -23
- package/src/__tests__/auto-topup-card.test.tsx +4 -12
- package/src/__tests__/backups-tab.test.tsx +3 -4
- package/src/__tests__/billing-layout-nav-hidden.test.tsx +5 -37
- package/src/__tests__/billing-payment-org-invoices.test.tsx +2 -18
- package/src/__tests__/billing.test.tsx +8 -39
- package/src/__tests__/bot-settings/resources-tab.test.tsx +1 -3
- package/src/__tests__/bot-settings/storage-tab.test.tsx +1 -3
- package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +1 -3
- package/src/__tests__/bot-settings-restart.test.tsx +1 -3
- package/src/__tests__/bot-settings.test.tsx +2 -6
- package/src/__tests__/brand.test.ts +6 -26
- package/src/__tests__/buy-credits-panel.test.tsx +1 -3
- package/src/__tests__/buy-crypto-credits-panel.test.tsx +101 -119
- package/src/__tests__/capability-conflicts.test.ts +2 -8
- package/src/__tests__/capability-resolver.test.tsx +2 -12
- package/src/__tests__/channel-wizard.test.tsx +4 -17
- package/src/__tests__/chat/chat-panel.test.tsx +1 -4
- package/src/__tests__/chat-store.test.ts +5 -15
- package/src/__tests__/command-center.test.tsx +10 -12
- package/src/__tests__/compliance-retention-edit.test.tsx +3 -6
- package/src/__tests__/confirmation-tracker.test.tsx +3 -18
- package/src/__tests__/coupon-input.test.tsx +1 -3
- package/src/__tests__/create-instance.test.tsx +1 -3
- package/src/__tests__/credit-balance.test.tsx +4 -12
- package/src/__tests__/credits.test.tsx +32 -85
- package/src/__tests__/email-verification-banner.test.tsx +2 -6
- package/src/__tests__/error-boundaries.test.tsx +0 -1
- package/src/__tests__/fetch-pricing.test.ts +2 -1
- package/src/__tests__/field-oauth.test.tsx +2 -6
- package/src/__tests__/fixtures/mock-manifests-data.js +1 -3
- package/src/__tests__/fixtures/mock-manifests.ts +2 -4
- package/src/__tests__/fleet-health-timestamp.test.tsx +1 -8
- package/src/__tests__/fleet-health-update.test.tsx +1 -8
- package/src/__tests__/gpu-dashboard.test.tsx +2 -6
- package/src/__tests__/instance-detail.test.tsx +3 -9
- package/src/__tests__/instance-list.test.tsx +1 -5
- package/src/__tests__/layout-snapshots.test.tsx +64 -11
- package/src/__tests__/marketplace-admin.test.tsx +2 -6
- package/src/__tests__/marketplace.test.tsx +11 -35
- package/src/__tests__/merge-api-rates.test.ts +1 -6
- package/src/__tests__/middleware.test.ts +32 -219
- package/src/__tests__/next-config-headers.test.ts +1 -3
- package/src/__tests__/notifications.test.tsx +4 -11
- package/src/__tests__/oauth-buttons.test.tsx +36 -59
- package/src/__tests__/oauth-error-mapping.test.tsx +2 -6
- package/src/__tests__/observability.test.tsx +23 -36
- package/src/__tests__/onboarding-page.test.tsx +4 -6
- package/src/__tests__/org-billing-api.test.tsx +1 -6
- package/src/__tests__/plugin-install-flow.test.tsx +28 -58
- package/src/__tests__/plugin-registry.test.tsx +3 -11
- package/src/__tests__/plugin-tool-sync.test.ts +1 -3
- package/src/__tests__/plugins-catalog-error.test.tsx +2 -6
- package/src/__tests__/plugins-toggle-race.test.tsx +3 -5
- package/src/__tests__/portfolio-chart.test.tsx +2 -6
- package/src/__tests__/promotion-form.test.tsx +2 -6
- package/src/__tests__/promotions-list.test.tsx +1 -3
- package/src/__tests__/provider-key-api.test.ts +2 -1
- package/src/__tests__/resend-verification-button.test.tsx +8 -24
- package/src/__tests__/secrets-audit-pagination.test.tsx +1 -3
- package/src/__tests__/settings.test.tsx +11 -21
- package/src/__tests__/setup-checklist.test.tsx +3 -9
- package/src/__tests__/setup.ts +25 -6
- package/src/__tests__/snapshot-api.test.ts +2 -1
- package/src/__tests__/step-superpowers.test.tsx +1 -3
- package/src/__tests__/tenant-context.test.tsx +1 -6
- package/src/__tests__/tenant-keys-api.test.ts +3 -4
- package/src/__tests__/tenant-table-pagination.test.tsx +2 -6
- package/src/__tests__/terminal-log-cleanup.test.tsx +0 -1
- package/src/__tests__/transaction-history.test.tsx +190 -238
- package/src/__tests__/trpc-types.test.ts +2 -6
- package/src/__tests__/use-chat.test.ts +1 -3
- package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +1 -4
- package/src/__tests__/use-sidecar-bridge.test.tsx +105 -0
- package/src/__tests__/use-webmcp.test.ts +1 -3
- package/src/__tests__/validate-elevenlabs-key.test.ts +2 -1
- package/src/__tests__/verify-page.test.tsx +4 -13
- package/src/__tests__/verify-redirect.test.tsx +2 -6
- package/src/app/(auth)/error.tsx +1 -7
- package/src/app/(auth)/forgot-password/page.tsx +4 -18
- package/src/app/(auth)/login/page.tsx +5 -22
- package/src/app/(auth)/reset-password/page.tsx +2 -12
- package/src/app/(auth)/signup/page.tsx +10 -44
- package/src/app/(auth)/verify/page.tsx +47 -0
- package/src/app/(dashboard)/billing/credits/page.tsx +14 -67
- package/src/app/(dashboard)/billing/error.tsx +2 -10
- package/src/app/(dashboard)/billing/layout.tsx +12 -62
- package/src/app/(dashboard)/billing/payment/page.tsx +17 -68
- package/src/app/(dashboard)/billing/plans/page.tsx +3 -9
- package/src/app/(dashboard)/billing/usage/hosted/page.tsx +8 -25
- package/src/app/(dashboard)/billing/usage/page.tsx +63 -103
- package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +9 -27
- package/src/app/(dashboard)/changesets/[id]/error.tsx +2 -6
- package/src/app/(dashboard)/changesets/error.tsx +1 -7
- package/src/app/(dashboard)/chat/page.tsx +2 -6
- package/src/app/(dashboard)/dashboard/network/page.tsx +5 -19
- package/src/app/(dashboard)/error.tsx +1 -7
- package/src/app/(dashboard)/layout.tsx +15 -36
- package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +14 -51
- package/src/app/(dashboard)/marketplace/error.tsx +1 -7
- package/src/app/(dashboard)/marketplace/page.tsx +6 -27
- package/src/app/(dashboard)/not-found.tsx +2 -5
- package/src/app/(dashboard)/onboarding/page.tsx +5 -22
- package/src/app/(dashboard)/settings/account/page.tsx +1 -6
- package/src/app/(dashboard)/settings/activity/page.tsx +8 -34
- package/src/app/(dashboard)/settings/api-keys/page.tsx +15 -60
- package/src/app/(dashboard)/settings/brain/page.tsx +9 -31
- package/src/app/(dashboard)/settings/error.tsx +2 -10
- package/src/app/(dashboard)/settings/notifications/page.tsx +2 -6
- package/src/app/(dashboard)/settings/org/page.tsx +13 -56
- package/src/app/(dashboard)/settings/page.tsx +1 -0
- package/src/app/(dashboard)/settings/profile/page.tsx +126 -73
- package/src/app/(dashboard)/settings/providers/page.tsx +21 -78
- package/src/app/(dashboard)/settings/secrets/page.tsx +13 -58
- package/src/app/(dashboard)/settings/security/page.tsx +31 -111
- package/src/app/admin/email-templates/email-templates-client.tsx +15 -58
- package/src/app/admin/error.tsx +1 -7
- package/src/app/admin/fleet-updates/error.tsx +1 -7
- package/src/app/admin/fleet-updates/fleet-updates-client.tsx +10 -50
- package/src/app/admin/layout.tsx +4 -0
- package/src/app/admin/payment-methods/page.tsx +9 -38
- package/src/app/admin/products/error.tsx +2 -7
- package/src/app/admin/products/page.tsx +1 -4
- package/src/app/admin/promotions/[id]/page.tsx +9 -38
- package/src/app/admin/promotions/page.tsx +9 -36
- package/src/app/admin/rate-overrides/page.tsx +9 -45
- package/src/app/auth/callback/[provider]/page.tsx +1 -8
- package/src/app/auth/verify/page.tsx +9 -36
- package/src/app/channels/error.tsx +2 -10
- package/src/app/channels/layout.tsx +9 -0
- package/src/app/channels/page.tsx +8 -20
- package/src/app/channels/setup/[plugin]/page.tsx +3 -5
- package/src/app/error.tsx +1 -7
- package/src/app/fleet/error.tsx +1 -7
- package/src/app/fleet/layout.tsx +5 -0
- package/src/app/fleet/settings/page.tsx +1 -3
- package/src/app/global-error.tsx +2 -10
- package/src/app/globals.css +1 -4
- package/src/app/instances/[id]/instance-detail-client.tsx +51 -125
- package/src/app/instances/error.tsx +2 -10
- package/src/app/instances/instance-list-client.tsx +20 -69
- package/src/app/instances/layout.tsx +9 -0
- package/src/app/instances/new/create-instance-client.tsx +10 -31
- package/src/app/layout.tsx +2 -10
- package/src/app/not-found.tsx +1 -3
- package/src/app/page.tsx +1 -2
- package/src/app/plugins/error.tsx +2 -10
- package/src/app/plugins/layout.tsx +5 -0
- package/src/app/plugins/page.tsx +16 -48
- package/src/app/pricing/error.tsx +1 -7
- package/src/app/privacy/page.tsx +93 -150
- package/src/app/status/error.tsx +1 -7
- package/src/app/terms/page.tsx +89 -144
- package/src/components/account-switcher.tsx +25 -52
- package/src/components/admin/accounting-dashboard.tsx +1 -3
- package/src/components/admin/admin-guard.tsx +1 -3
- package/src/components/admin/admin-nav.tsx +1 -3
- package/src/components/admin/affiliate-dashboard.tsx +25 -94
- package/src/components/admin/audit-log-table.tsx +13 -49
- package/src/components/admin/billing-health-dashboard.tsx +7 -25
- package/src/components/admin/bulk-actions-bar.test.tsx +1 -7
- package/src/components/admin/bulk-actions-bar.tsx +1 -3
- package/src/components/admin/bulk-export-dialog.test.tsx +1 -7
- package/src/components/admin/bulk-export-dialog.tsx +6 -32
- package/src/components/admin/bulk-grant-dialog.test.tsx +2 -6
- package/src/components/admin/bulk-grant-dialog.tsx +4 -15
- package/src/components/admin/bulk-preview-dialog.tsx +3 -12
- package/src/components/admin/bulk-reactivate-dialog.tsx +1 -7
- package/src/components/admin/bulk-select-all-banner.tsx +1 -6
- package/src/components/admin/bulk-suspend-dialog.tsx +5 -12
- package/src/components/admin/bulk-undo-toast.tsx +1 -2
- package/src/components/admin/compliance-dashboard.tsx +31 -101
- package/src/components/admin/gpu-dashboard.tsx +21 -70
- package/src/components/admin/grant-credits-dialog.tsx +4 -17
- package/src/components/admin/incident-dashboard.tsx +10 -25
- package/src/components/admin/inference-dashboard.tsx +14 -54
- package/src/components/admin/marketplace-admin.tsx +18 -60
- package/src/components/admin/migrations-dashboard.tsx +9 -42
- package/src/components/admin/onboarding-dashboard.tsx +14 -64
- package/src/components/admin/pool-config-dashboard.tsx +4 -10
- package/src/components/admin/products/fleet-form.tsx +2 -11
- package/src/components/admin/products/nav-editor.tsx +3 -10
- package/src/components/admin/promotions/promotion-form.tsx +9 -42
- package/src/components/admin/roles-dashboard.tsx +7 -34
- package/src/components/admin/suspend-dialog.tsx +4 -11
- package/src/components/admin/tenant-notes-panel.tsx +1 -3
- package/src/components/admin/tenant-row-actions.tsx +4 -20
- package/src/components/admin/tenant-table.tsx +12 -49
- package/src/components/auth/auth-redirect.tsx +11 -3
- package/src/components/auth/email-verification-result-banner.tsx +1 -3
- package/src/components/auth/resend-verification-button.tsx +2 -10
- package/src/components/auth/wopr-wordmark.tsx +1 -3
- package/src/components/billing/add-payment-method-dialog.tsx +1 -2
- package/src/components/billing/affiliate-dashboard.tsx +4 -16
- package/src/components/billing/amount-selector.tsx +1 -3
- package/src/components/billing/auto-topup-card.tsx +2 -11
- package/src/components/billing/buy-credits-panel.tsx +14 -17
- package/src/components/billing/byok-callout.tsx +6 -8
- package/src/components/billing/confirmation-tracker.tsx +4 -14
- package/src/components/billing/credit-balance-badge.tsx +22 -0
- package/src/components/billing/credit-balance.tsx +3 -9
- package/src/components/billing/crypto-checkout.tsx +5 -24
- package/src/components/billing/degraded-state-banner.tsx +1 -3
- package/src/components/billing/deposit-view.tsx +301 -41
- package/src/components/billing/dividend-banner.tsx +1 -3
- package/src/components/billing/dividend-eligibility.tsx +3 -12
- package/src/components/billing/dividend-pool-stats.tsx +6 -20
- package/src/components/billing/first-dividend-dialog.tsx +2 -2
- package/src/components/billing/org-billing-page.tsx +8 -31
- package/src/components/billing/payment-method-picker.tsx +2 -10
- package/src/components/billing/suspension-banner.tsx +2 -7
- package/src/components/billing/transaction-history.tsx +10 -58
- package/src/components/billing/unified-checkout.tsx +547 -0
- package/src/components/bot-settings/backups-tab.tsx +9 -33
- package/src/components/bot-settings/bot-settings-client.tsx +32 -134
- package/src/components/bot-settings/resources-tab.tsx +2 -9
- package/src/components/bot-settings/storage-tab.tsx +19 -48
- package/src/components/bot-settings/vps-info-panel.tsx +3 -11
- package/src/components/bot-settings/vps-upgrade-card.tsx +3 -4
- package/src/components/brand-hydrator.tsx +13 -0
- package/src/components/channel-wizard/field-interactive.tsx +1 -3
- package/src/components/channel-wizard/field-qr.tsx +10 -39
- package/src/components/channel-wizard/step-renderer.tsx +5 -28
- package/src/components/channel-wizard/wizard.tsx +6 -31
- package/src/components/chat/chat-message.tsx +1 -4
- package/src/components/chat/chat-panel.tsx +4 -18
- package/src/components/chat/chat-widget.tsx +3 -14
- package/src/components/dashboard/command-center.tsx +15 -61
- package/src/components/fleet/update-settings-card.tsx +7 -23
- package/src/components/instance-update-banner.tsx +130 -0
- package/src/components/instances/friends-tab.test.tsx +2 -9
- package/src/components/instances/friends-tab.tsx +18 -74
- package/src/components/instances/update-available-badge.tsx +2 -11
- package/src/components/landing/hero.tsx +3 -9
- package/src/components/landing/landing-page.tsx +1 -3
- package/src/components/landing/portfolio-chart.tsx +4 -9
- package/src/components/landing/story-sections.tsx +1 -3
- package/src/components/landing/terminal-sequence.tsx +4 -17
- package/src/components/marketplace/empty-state.tsx +2 -6
- package/src/components/marketplace/first-visit-hero.tsx +1 -3
- package/src/components/marketplace/install-wizard.tsx +20 -77
- package/src/components/marketplace/marketplace-tabs.tsx +1 -4
- package/src/components/marketplace/plugin-card.tsx +2 -9
- package/src/components/marketplace/superpower-content.tsx +1 -3
- package/src/components/marketplace/terminal-search.tsx +2 -8
- package/src/components/oauth-buttons.tsx +29 -14
- package/src/components/observability/fleet-health.tsx +5 -18
- package/src/components/observability/health-overview.tsx +7 -20
- package/src/components/observability/logs-viewer.tsx +8 -32
- package/src/components/observability/metrics-dashboard.tsx +2 -15
- package/src/components/onboarding/fallback-setup.tsx +6 -25
- package/src/components/onboarding/setup-checklist.tsx +18 -51
- package/src/components/onboarding/step-superpowers.tsx +1 -4
- package/src/components/plugin-setup/setup-chat-panel.tsx +6 -22
- package/src/components/pricing/dividend-calculator.tsx +6 -12
- package/src/components/pricing/dividend-stats.tsx +5 -17
- package/src/components/pricing/pricing-page.tsx +17 -36
- package/src/components/settings/create-org-wizard.tsx +2 -5
- package/src/components/sidebar.tsx +7 -42
- package/src/components/sidecar-frame.tsx +78 -0
- package/src/components/status/status-page.tsx +6 -28
- package/src/components/ui/alert-dialog.tsx +8 -25
- package/src/components/ui/badge.tsx +2 -8
- package/src/components/ui/banner.tsx +1 -6
- package/src/components/ui/card.tsx +5 -24
- package/src/components/ui/checkbox.tsx +1 -5
- package/src/components/ui/collapsible.tsx +3 -8
- package/src/components/ui/dialog.tsx +4 -10
- package/src/components/ui/dropdown-menu.tsx +9 -18
- package/src/components/ui/form.tsx +2 -16
- package/src/components/ui/popover.tsx +3 -23
- package/src/components/ui/progress.tsx +1 -5
- package/src/components/ui/radio-group.tsx +3 -15
- package/src/components/ui/select.tsx +4 -17
- package/src/components/ui/sheet.tsx +5 -19
- package/src/components/ui/skeleton.tsx +1 -7
- package/src/components/ui/table.tsx +5 -22
- package/src/components/ui/tabs.tsx +3 -13
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/components/unified-sidebar.tsx +493 -0
- package/src/hooks/__tests__/use-fleet-sse.test.ts +1 -4
- package/src/hooks/__tests__/use-save-queue.test.ts +2 -8
- package/src/hooks/use-credit-balance.ts +27 -0
- package/src/hooks/use-my-org-role.ts +1 -3
- package/src/hooks/use-plugin-registry.ts +8 -14
- package/src/hooks/use-plugin-setup-chat.ts +2 -5
- package/src/hooks/use-sidecar-bridge.tsx +148 -0
- package/src/hooks/use-webmcp.ts +1 -4
- package/src/lib/__tests__/admin-api.test.ts +1 -3
- package/src/lib/__tests__/api-bot-crud.test.ts +8 -18
- package/src/lib/__tests__/api-fetch.test.ts +4 -16
- package/src/lib/__tests__/org-billing-api.test.ts +1 -3
- package/src/lib/__tests__/pricing-data.test.ts +0 -8
- package/src/lib/__tests__/settings-api.test.ts +1 -3
- package/src/lib/admin-affiliate-api.ts +2 -7
- package/src/lib/admin-api.ts +6 -26
- package/src/lib/admin-incident-api.ts +11 -19
- package/src/lib/admin-marketplace-api.ts +1 -5
- package/src/lib/api-config.test.ts +5 -50
- package/src/lib/api.ts +143 -122
- package/src/lib/auth-client.ts +1 -2
- package/src/lib/bot-settings-data.ts +11 -36
- package/src/lib/brand-config.ts +56 -115
- package/src/lib/brand.ts +2 -15
- package/src/lib/chat/use-chat.ts +2 -7
- package/src/lib/cost-comparison-data.test.ts +1 -3
- package/src/lib/cost-comparison-data.ts +1 -4
- package/src/lib/errors.ts +1 -4
- package/src/lib/fetch-utils.test.ts +26 -9
- package/src/lib/fetch-utils.ts +40 -11
- package/src/lib/logger.ts +2 -0
- package/src/lib/marketplace-data.ts +3 -11
- package/src/lib/oauth-errors.ts +2 -4
- package/src/lib/onboarding-data.ts +3 -11
- package/src/lib/org-api.ts +2 -10
- package/src/lib/org-billing-api.ts +5 -19
- package/src/lib/plugin/tool-definitions.ts +1 -2
- package/src/lib/require-auth.ts +57 -0
- package/src/lib/settings-api.ts +1 -4
- package/src/lib/sidecar-routes.ts +43 -0
- package/src/lib/trpc-server.ts +49 -0
- package/src/lib/trpc-types.ts +4 -6
- package/src/lib/trpc.tsx +12 -4
- package/src/lib/validate-redirect-url.ts +1 -4
- package/src/lib/webmcp/marketplace-onboarding-tools.ts +6 -16
- package/src/lib/webmcp/register.ts +1 -4
- package/src/lib/webmcp/tools.ts +2 -9
- package/src/proxy.ts +35 -212
- package/src/types/missing-deps.d.ts +2 -8
- package/tsconfig.json +1 -8
- package/biome.json +0 -52
- package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +0 -741
- package/src/__tests__/billing-byok-callout.test.tsx +0 -76
- package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +0 -112
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render } from "@testing-library/react";
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
// --- Mocks ---
|
|
@@ -15,7 +15,11 @@ vi.mock("next/font/google", () => ({
|
|
|
15
15
|
|
|
16
16
|
vi.mock("better-auth/react", () => ({
|
|
17
17
|
createAuthClient: () => ({
|
|
18
|
-
useSession: () => ({
|
|
18
|
+
useSession: () => ({
|
|
19
|
+
data: { user: { id: "u1", email: "test@test.com" }, session: { id: "s1" } },
|
|
20
|
+
isPending: false,
|
|
21
|
+
error: null,
|
|
22
|
+
}),
|
|
19
23
|
signIn: { email: vi.fn(), social: vi.fn() },
|
|
20
24
|
signUp: { email: vi.fn() },
|
|
21
25
|
signOut: vi.fn(),
|
|
@@ -23,10 +27,30 @@ vi.mock("better-auth/react", () => ({
|
|
|
23
27
|
}));
|
|
24
28
|
|
|
25
29
|
vi.mock("@/lib/auth-client", () => ({
|
|
26
|
-
useSession: () => ({
|
|
30
|
+
useSession: () => ({
|
|
31
|
+
data: { user: { id: "u1", email: "test@test.com" }, session: { id: "s1" } },
|
|
32
|
+
isPending: false,
|
|
33
|
+
error: null,
|
|
34
|
+
}),
|
|
27
35
|
signOut: vi.fn(),
|
|
28
36
|
}));
|
|
29
37
|
|
|
38
|
+
vi.mock("@/lib/require-auth", () => ({
|
|
39
|
+
useRequireAuth: () => ({
|
|
40
|
+
user: { id: "u1", email: "test@test.com" },
|
|
41
|
+
session: { id: "s1" },
|
|
42
|
+
isPending: false,
|
|
43
|
+
isAuthed: true,
|
|
44
|
+
}),
|
|
45
|
+
useRequireAdmin: () => ({
|
|
46
|
+
user: { id: "u1", email: "test@test.com", role: "platform_admin" },
|
|
47
|
+
session: { id: "s1" },
|
|
48
|
+
isPending: false,
|
|
49
|
+
isAuthed: true,
|
|
50
|
+
isAdmin: true,
|
|
51
|
+
}),
|
|
52
|
+
}));
|
|
53
|
+
|
|
30
54
|
vi.mock("@/components/sidebar", () => ({
|
|
31
55
|
Sidebar: () => <div data-testid="sidebar">Sidebar</div>,
|
|
32
56
|
SidebarContent: ({ onNavigate: _o }: { onNavigate?: () => void }) => (
|
|
@@ -62,6 +86,22 @@ vi.mock("@/lib/api", () => ({
|
|
|
62
86
|
vi.mock("@/lib/api-config", () => ({
|
|
63
87
|
SITE_URL: "https://localhost",
|
|
64
88
|
PLATFORM_BASE_URL: "https://localhost",
|
|
89
|
+
API_BASE_URL: "https://localhost/api",
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
vi.mock("@/lib/brand-config", () => ({
|
|
93
|
+
getBrandConfig: () => ({
|
|
94
|
+
chatEnabled: true,
|
|
95
|
+
tenantCookieName: "platform_tenant_id",
|
|
96
|
+
homePath: "/marketplace",
|
|
97
|
+
productName: "Platform",
|
|
98
|
+
brandName: "Platform",
|
|
99
|
+
domain: "localhost",
|
|
100
|
+
navItems: [],
|
|
101
|
+
}),
|
|
102
|
+
productName: () => "Platform",
|
|
103
|
+
brandName: () => "Platform",
|
|
104
|
+
setBrandConfig: vi.fn(),
|
|
65
105
|
}));
|
|
66
106
|
|
|
67
107
|
vi.mock("@/components/auth/email-verification-banner", () => ({
|
|
@@ -102,7 +142,8 @@ describe("Layout snapshots", () => {
|
|
|
102
142
|
<div>auth child</div>
|
|
103
143
|
</AuthLayout>,
|
|
104
144
|
);
|
|
105
|
-
expect(container).
|
|
145
|
+
expect(container.querySelector(".min-h-screen")).not.toBeNull();
|
|
146
|
+
expect(container.textContent).toContain("auth child");
|
|
106
147
|
});
|
|
107
148
|
|
|
108
149
|
it("DashboardLayout renders sidebar + main with mobile sheet", async () => {
|
|
@@ -112,7 +153,12 @@ describe("Layout snapshots", () => {
|
|
|
112
153
|
<div>dashboard child</div>
|
|
113
154
|
</DashboardLayout>,
|
|
114
155
|
);
|
|
115
|
-
expect(
|
|
156
|
+
expect(screen.getByTestId("sidebar")).not.toBeNull();
|
|
157
|
+
expect(container.textContent).toContain("dashboard child");
|
|
158
|
+
// Desktop layout
|
|
159
|
+
expect(container.querySelector(".hidden.lg\\:flex")).not.toBeNull();
|
|
160
|
+
// Mobile layout
|
|
161
|
+
expect(container.querySelector(".lg\\:hidden")).not.toBeNull();
|
|
116
162
|
});
|
|
117
163
|
|
|
118
164
|
it("FleetLayout renders sidebar + main", async () => {
|
|
@@ -122,7 +168,8 @@ describe("Layout snapshots", () => {
|
|
|
122
168
|
<div>fleet child</div>
|
|
123
169
|
</FleetLayout>,
|
|
124
170
|
);
|
|
125
|
-
expect(
|
|
171
|
+
expect(screen.getByTestId("sidebar")).not.toBeNull();
|
|
172
|
+
expect(container.textContent).toContain("fleet child");
|
|
126
173
|
});
|
|
127
174
|
|
|
128
175
|
it("PluginsLayout renders sidebar + main", async () => {
|
|
@@ -132,7 +179,8 @@ describe("Layout snapshots", () => {
|
|
|
132
179
|
<div>plugins child</div>
|
|
133
180
|
</PluginsLayout>,
|
|
134
181
|
);
|
|
135
|
-
expect(
|
|
182
|
+
expect(screen.getByTestId("sidebar")).not.toBeNull();
|
|
183
|
+
expect(container.textContent).toContain("plugins child");
|
|
136
184
|
});
|
|
137
185
|
|
|
138
186
|
it("AdminLayout renders desktop sidebar + admin nav + mobile fallback", async () => {
|
|
@@ -142,7 +190,9 @@ describe("Layout snapshots", () => {
|
|
|
142
190
|
<div>admin child</div>
|
|
143
191
|
</AdminLayout>,
|
|
144
192
|
);
|
|
145
|
-
expect(
|
|
193
|
+
expect(screen.getByTestId("sidebar")).not.toBeNull();
|
|
194
|
+
expect(screen.getByTestId("admin-nav")).not.toBeNull();
|
|
195
|
+
expect(container.textContent).toContain("admin child");
|
|
146
196
|
});
|
|
147
197
|
|
|
148
198
|
it("SettingsLayout renders desktop nav + mobile sheet", async () => {
|
|
@@ -152,16 +202,19 @@ describe("Layout snapshots", () => {
|
|
|
152
202
|
<div>settings child</div>
|
|
153
203
|
</SettingsLayout>,
|
|
154
204
|
);
|
|
155
|
-
expect(container).
|
|
205
|
+
expect(container.textContent).toContain("settings child");
|
|
206
|
+
expect(container.textContent).toContain("Settings");
|
|
156
207
|
});
|
|
157
208
|
|
|
158
|
-
it("BillingLayout renders
|
|
209
|
+
it("BillingLayout renders content area + footer", async () => {
|
|
159
210
|
const { default: BillingLayout } = await import("@/app/(dashboard)/billing/layout");
|
|
160
211
|
const { container } = render(
|
|
161
212
|
<BillingLayout>
|
|
162
213
|
<div>billing child</div>
|
|
163
214
|
</BillingLayout>,
|
|
164
215
|
);
|
|
165
|
-
expect(container).
|
|
216
|
+
expect(container.textContent).toContain("billing child");
|
|
217
|
+
expect(container.textContent).toContain("Terms of Service");
|
|
218
|
+
expect(container.textContent).toContain("Privacy Policy");
|
|
166
219
|
});
|
|
167
220
|
});
|
|
@@ -113,9 +113,7 @@ describe("admin-marketplace-api", () => {
|
|
|
113
113
|
const { updatePlugin } = await import("../lib/admin-marketplace-api");
|
|
114
114
|
mockUpdatePlugin.mockRejectedValue(new Error("tRPC unavailable"));
|
|
115
115
|
|
|
116
|
-
await expect(updatePlugin({ id: "discord", notes: "fallback note" })).rejects.toThrow(
|
|
117
|
-
"tRPC unavailable",
|
|
118
|
-
);
|
|
116
|
+
await expect(updatePlugin({ id: "discord", notes: "fallback note" })).rejects.toThrow("tRPC unavailable");
|
|
119
117
|
});
|
|
120
118
|
});
|
|
121
119
|
|
|
@@ -143,9 +141,7 @@ describe("admin-marketplace-api", () => {
|
|
|
143
141
|
const { addPluginByNpm } = await import("../lib/admin-marketplace-api");
|
|
144
142
|
mockAddPlugin.mockRejectedValue(new Error("tRPC unavailable"));
|
|
145
143
|
|
|
146
|
-
await expect(addPluginByNpm({ npm_package: "@wopr-network/plugin-test" })).rejects.toThrow(
|
|
147
|
-
"tRPC unavailable",
|
|
148
|
-
);
|
|
144
|
+
await expect(addPluginByNpm({ npm_package: "@wopr-network/plugin-test" })).rejects.toThrow("tRPC unavailable");
|
|
149
145
|
});
|
|
150
146
|
});
|
|
151
147
|
});
|
|
@@ -33,11 +33,7 @@ vi.mock("next/navigation", () => ({
|
|
|
33
33
|
|
|
34
34
|
// --- Mock next/link ---
|
|
35
35
|
vi.mock("next/link", () => ({
|
|
36
|
-
default: ({
|
|
37
|
-
children,
|
|
38
|
-
href,
|
|
39
|
-
...props
|
|
40
|
-
}: { children: React.ReactNode; href: string } & Record<string, unknown>) => (
|
|
36
|
+
default: ({ children, href, ...props }: { children: React.ReactNode; href: string } & Record<string, unknown>) => (
|
|
41
37
|
<a href={href} {...props}>
|
|
42
38
|
{children}
|
|
43
39
|
</a>
|
|
@@ -307,9 +303,7 @@ describe("PluginDetailPage", () => {
|
|
|
307
303
|
|
|
308
304
|
it("renders plugin detail page with manifest info", async () => {
|
|
309
305
|
mockParams.plugin = "discord";
|
|
310
|
-
const { default: PluginDetailPage } = await import(
|
|
311
|
-
"../app/(dashboard)/marketplace/[plugin]/page"
|
|
312
|
-
);
|
|
306
|
+
const { default: PluginDetailPage } = await import("../app/(dashboard)/marketplace/[plugin]/page");
|
|
313
307
|
renderWithQueryClient(<PluginDetailPage />);
|
|
314
308
|
|
|
315
309
|
// Wait for loading
|
|
@@ -321,9 +315,7 @@ describe("PluginDetailPage", () => {
|
|
|
321
315
|
|
|
322
316
|
it("shows not found for invalid plugin id", async () => {
|
|
323
317
|
mockParams.plugin = "nonexistent-plugin";
|
|
324
|
-
const { default: PluginDetailPage } = await import(
|
|
325
|
-
"../app/(dashboard)/marketplace/[plugin]/page"
|
|
326
|
-
);
|
|
318
|
+
const { default: PluginDetailPage } = await import("../app/(dashboard)/marketplace/[plugin]/page");
|
|
327
319
|
renderWithQueryClient(<PluginDetailPage />);
|
|
328
320
|
|
|
329
321
|
expect(await screen.findByText("Plugin not found.")).toBeInTheDocument();
|
|
@@ -331,9 +323,7 @@ describe("PluginDetailPage", () => {
|
|
|
331
323
|
|
|
332
324
|
it("shows hosted adapter info for eligible plugins", async () => {
|
|
333
325
|
mockParams.plugin = "semantic-memory";
|
|
334
|
-
const { default: PluginDetailPage } = await import(
|
|
335
|
-
"../app/(dashboard)/marketplace/[plugin]/page"
|
|
336
|
-
);
|
|
326
|
+
const { default: PluginDetailPage } = await import("../app/(dashboard)/marketplace/[plugin]/page");
|
|
337
327
|
renderWithQueryClient(<PluginDetailPage />);
|
|
338
328
|
|
|
339
329
|
await screen.findByText("A Bot That Never Forgets");
|
|
@@ -344,9 +334,7 @@ describe("PluginDetailPage", () => {
|
|
|
344
334
|
|
|
345
335
|
it("shows requirements for plugins with dependencies", async () => {
|
|
346
336
|
mockParams.plugin = "meeting-transcriber";
|
|
347
|
-
const { default: PluginDetailPage } = await import(
|
|
348
|
-
"../app/(dashboard)/marketplace/[plugin]/page"
|
|
349
|
-
);
|
|
337
|
+
const { default: PluginDetailPage } = await import("../app/(dashboard)/marketplace/[plugin]/page");
|
|
350
338
|
renderWithQueryClient(<PluginDetailPage />);
|
|
351
339
|
|
|
352
340
|
await screen.findByText("Fire Your Secretary");
|
|
@@ -356,9 +344,7 @@ describe("PluginDetailPage", () => {
|
|
|
356
344
|
it("opens install wizard when Install button is clicked", async () => {
|
|
357
345
|
const user = userEvent.setup();
|
|
358
346
|
mockParams.plugin = "webhooks";
|
|
359
|
-
const { default: PluginDetailPage } = await import(
|
|
360
|
-
"../app/(dashboard)/marketplace/[plugin]/page"
|
|
361
|
-
);
|
|
347
|
+
const { default: PluginDetailPage } = await import("../app/(dashboard)/marketplace/[plugin]/page");
|
|
362
348
|
renderWithQueryClient(<PluginDetailPage />);
|
|
363
349
|
|
|
364
350
|
await screen.findByText("Webhooks");
|
|
@@ -371,9 +357,7 @@ describe("PluginDetailPage", () => {
|
|
|
371
357
|
|
|
372
358
|
it("shows changelog entries", async () => {
|
|
373
359
|
mockParams.plugin = "discord";
|
|
374
|
-
const { default: PluginDetailPage } = await import(
|
|
375
|
-
"../app/(dashboard)/marketplace/[plugin]/page"
|
|
376
|
-
);
|
|
360
|
+
const { default: PluginDetailPage } = await import("../app/(dashboard)/marketplace/[plugin]/page");
|
|
377
361
|
renderWithQueryClient(<PluginDetailPage />);
|
|
378
362
|
|
|
379
363
|
await screen.findByText("Discord");
|
|
@@ -389,9 +373,7 @@ describe("PluginDetailPage", () => {
|
|
|
389
373
|
|
|
390
374
|
it("shows configuration schema", async () => {
|
|
391
375
|
mockParams.plugin = "discord";
|
|
392
|
-
const { default: PluginDetailPage } = await import(
|
|
393
|
-
"../app/(dashboard)/marketplace/[plugin]/page"
|
|
394
|
-
);
|
|
376
|
+
const { default: PluginDetailPage } = await import("../app/(dashboard)/marketplace/[plugin]/page");
|
|
395
377
|
renderWithQueryClient(<PluginDetailPage />);
|
|
396
378
|
|
|
397
379
|
await screen.findByText("Discord");
|
|
@@ -405,9 +387,7 @@ describe("PluginDetailPage", () => {
|
|
|
405
387
|
});
|
|
406
388
|
|
|
407
389
|
describe("InstallWizard", () => {
|
|
408
|
-
const mockBots = [
|
|
409
|
-
{ id: "00000000-0000-4000-8000-000000000001", name: "My Bot", state: "running" },
|
|
410
|
-
];
|
|
390
|
+
const mockBots = [{ id: "00000000-0000-4000-8000-000000000001", name: "My Bot", state: "running" }];
|
|
411
391
|
|
|
412
392
|
beforeEach(() => {
|
|
413
393
|
// Mock fetch for listBots so the wizard doesn't hang on network calls
|
|
@@ -458,9 +438,7 @@ describe("InstallWizard", () => {
|
|
|
458
438
|
|
|
459
439
|
// First phase is bot-select — wait for bots to load and select one
|
|
460
440
|
const user = userEvent.setup();
|
|
461
|
-
expect(
|
|
462
|
-
await screen.findByText("Select which bot to install this plugin on"),
|
|
463
|
-
).toBeInTheDocument();
|
|
441
|
+
expect(await screen.findByText("Select which bot to install this plugin on")).toBeInTheDocument();
|
|
464
442
|
|
|
465
443
|
// Wait for bots to load
|
|
466
444
|
const botButton = await screen.findByText("My Bot");
|
|
@@ -477,9 +455,7 @@ describe("InstallWizard", () => {
|
|
|
477
455
|
|
|
478
456
|
// Should now show provider selector
|
|
479
457
|
expect(
|
|
480
|
-
screen.getByText(
|
|
481
|
-
"Some capabilities can be provided by Platform Hosted services. Choose for each:",
|
|
482
|
-
),
|
|
458
|
+
screen.getByText("Some capabilities can be provided by Platform Hosted services. Choose for each:"),
|
|
483
459
|
).toBeInTheDocument();
|
|
484
460
|
expect(screen.getByText("LLM")).toBeInTheDocument();
|
|
485
461
|
expect(screen.getByText("STT")).toBeInTheDocument();
|
|
@@ -21,12 +21,7 @@ describe("mergeApiRates", () => {
|
|
|
21
21
|
tts: [{ name: "TTS", unit: "1K chars", price: 0.2 }],
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
expect(result.map((c) => c.category)).toEqual([
|
|
25
|
-
"Text Generation",
|
|
26
|
-
"Voice",
|
|
27
|
-
"Image Generation",
|
|
28
|
-
"Messaging",
|
|
29
|
-
]);
|
|
24
|
+
expect(result.map((c) => c.category)).toEqual(["Text Generation", "Voice", "Image Generation", "Messaging"]);
|
|
30
25
|
});
|
|
31
26
|
|
|
32
27
|
it("handles unknown capability keys with fallback", () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest } from "next/server";
|
|
2
|
-
import { afterEach,
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import middleware, { config, validateCsrfOrigin } from "../proxy";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -38,118 +38,36 @@ function buildRequest(
|
|
|
38
38
|
return req;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function isRedirect(res: Response): boolean {
|
|
42
|
-
return res.status >= 300 && res.status < 400;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function redirectPath(res: Response): string {
|
|
46
|
-
const loc = res.headers.get("location");
|
|
47
|
-
if (!loc) return "";
|
|
48
|
-
try {
|
|
49
|
-
const u = new URL(loc);
|
|
50
|
-
return u.pathname + (u.search ? u.search : "");
|
|
51
|
-
} catch {
|
|
52
|
-
// Non-URL location string (e.g. relative path) — return as-is
|
|
53
|
-
return loc;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
function isPassThrough(res: Response): boolean {
|
|
58
42
|
return res.headers.get("x-middleware-next") === "1";
|
|
59
43
|
}
|
|
60
44
|
|
|
61
45
|
describe("middleware", () => {
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
// Reset to a safe no-session mock by default.
|
|
64
|
-
// Tests that need fetch (admin routes) stub it themselves.
|
|
65
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false }));
|
|
66
|
-
delete process.env.NEXT_PUBLIC_APP_DOMAIN;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
46
|
afterEach(() => {
|
|
70
47
|
vi.unstubAllGlobals();
|
|
71
|
-
delete process.env.NEXT_PUBLIC_APP_DOMAIN;
|
|
72
48
|
});
|
|
73
49
|
|
|
74
50
|
// ---------------------------------------------------------------------------
|
|
75
|
-
//
|
|
51
|
+
// Pass-through with CSP (middleware no longer does auth — useRequireAuth handles it)
|
|
76
52
|
// ---------------------------------------------------------------------------
|
|
77
|
-
describe("
|
|
78
|
-
it("
|
|
53
|
+
describe("pass-through with CSP", () => {
|
|
54
|
+
it("passes /dashboard through without session cookie", async () => {
|
|
79
55
|
const req = buildRequest("/dashboard");
|
|
80
56
|
const res = await middleware(req);
|
|
81
|
-
expect(
|
|
82
|
-
|
|
83
|
-
expect(loc).toContain("/login");
|
|
84
|
-
expect(loc).toContain("callbackUrl=%2Fdashboard");
|
|
57
|
+
expect(isPassThrough(res)).toBe(true);
|
|
58
|
+
expect(res.headers.get("content-security-policy")).not.toBeNull();
|
|
85
59
|
});
|
|
86
60
|
|
|
87
|
-
it("
|
|
61
|
+
it("passes /marketplace through without session cookie", async () => {
|
|
88
62
|
const req = buildRequest("/marketplace");
|
|
89
63
|
const res = await middleware(req);
|
|
90
|
-
expect(
|
|
91
|
-
expect(redirectPath(res)).toContain("/login");
|
|
92
|
-
expect(redirectPath(res)).toContain("callbackUrl=%2Fmarketplace");
|
|
64
|
+
expect(isPassThrough(res)).toBe(true);
|
|
93
65
|
});
|
|
94
66
|
|
|
95
|
-
it("
|
|
67
|
+
it("passes /settings through without session cookie", async () => {
|
|
96
68
|
const req = buildRequest("/settings");
|
|
97
69
|
const res = await middleware(req);
|
|
98
|
-
expect(
|
|
99
|
-
expect(redirectPath(res)).toContain("/login");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("does not treat an empty session cookie as authenticated", async () => {
|
|
103
|
-
const req = buildRequest("/dashboard", {
|
|
104
|
-
cookies: { "better-auth.session_token": "" },
|
|
105
|
-
});
|
|
106
|
-
const res = await middleware(req);
|
|
107
|
-
expect(isRedirect(res)).toBe(true);
|
|
108
|
-
expect(redirectPath(res)).toContain("/login");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("does not treat a whitespace-only session cookie as authenticated", async () => {
|
|
112
|
-
const req = buildRequest("/dashboard", {
|
|
113
|
-
cookies: { "better-auth.session_token": " " },
|
|
114
|
-
});
|
|
115
|
-
const res = await middleware(req);
|
|
116
|
-
expect(isRedirect(res)).toBe(true);
|
|
117
|
-
expect(redirectPath(res)).toContain("/login");
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// Open redirect prevention — callbackUrl sanitization
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
describe("callbackUrl sanitization (open redirect prevention)", () => {
|
|
125
|
-
it("sanitizes percent-encoded protocol-relative pathname in callbackUrl", async () => {
|
|
126
|
-
// /%2F%2Fevil-redirect decodes to //evil-redirect — must be rejected.
|
|
127
|
-
// Path has no dot so it reaches the session check (not static-file bypass).
|
|
128
|
-
const req = buildRequest("/%2F%2Fevil-redirect");
|
|
129
|
-
const res = await middleware(req);
|
|
130
|
-
expect(isRedirect(res)).toBe(true);
|
|
131
|
-
const loc = redirectPath(res);
|
|
132
|
-
expect(loc).toContain("/login");
|
|
133
|
-
// callbackUrl must be "/" (sanitized), not the malicious path
|
|
134
|
-
expect(loc).toContain("callbackUrl=%2F");
|
|
135
|
-
expect(loc).not.toContain("evil-redirect");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("preserves legitimate callbackUrl for normal paths", async () => {
|
|
139
|
-
const req = buildRequest("/settings/profile");
|
|
140
|
-
const res = await middleware(req);
|
|
141
|
-
expect(isRedirect(res)).toBe(true);
|
|
142
|
-
const loc = redirectPath(res);
|
|
143
|
-
expect(loc).toContain("callbackUrl=%2Fsettings%2Fprofile");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("sanitizes mixed-case percent-encoded bypass attempts", async () => {
|
|
147
|
-
// /%2f%2Fevil-redirect also decodes to //evil-redirect — path has no dot.
|
|
148
|
-
const req = buildRequest("/%2f%2Fevil-redirect");
|
|
149
|
-
const res = await middleware(req);
|
|
150
|
-
expect(isRedirect(res)).toBe(true);
|
|
151
|
-
const loc = redirectPath(res);
|
|
152
|
-
expect(loc).not.toContain("evil-redirect");
|
|
70
|
+
expect(isPassThrough(res)).toBe(true);
|
|
153
71
|
});
|
|
154
72
|
});
|
|
155
73
|
|
|
@@ -277,68 +195,42 @@ describe("middleware", () => {
|
|
|
277
195
|
expect(isPassThrough(res)).toBe(true);
|
|
278
196
|
});
|
|
279
197
|
|
|
280
|
-
it("
|
|
198
|
+
it("passes /terms/extra through (auth handled client-side)", async () => {
|
|
281
199
|
const req = buildRequest("/terms/extra");
|
|
282
200
|
const res = await middleware(req);
|
|
283
|
-
expect(
|
|
284
|
-
expect(redirectPath(res)).toContain("/login");
|
|
201
|
+
expect(isPassThrough(res)).toBe(true);
|
|
285
202
|
});
|
|
286
203
|
|
|
287
|
-
it("
|
|
204
|
+
it("passes /pricing/enterprise through (auth handled client-side)", async () => {
|
|
288
205
|
const req = buildRequest("/pricing/enterprise");
|
|
289
206
|
const res = await middleware(req);
|
|
290
|
-
expect(
|
|
291
|
-
expect(redirectPath(res)).toContain("/login");
|
|
207
|
+
expect(isPassThrough(res)).toBe(true);
|
|
292
208
|
});
|
|
293
209
|
|
|
294
|
-
it("
|
|
210
|
+
it("passes /privacy/policy through (auth handled client-side)", async () => {
|
|
295
211
|
const req = buildRequest("/privacy/policy");
|
|
296
212
|
const res = await middleware(req);
|
|
297
|
-
expect(
|
|
298
|
-
expect(redirectPath(res)).toContain("/login");
|
|
213
|
+
expect(isPassThrough(res)).toBe(true);
|
|
299
214
|
});
|
|
300
215
|
});
|
|
301
216
|
|
|
302
217
|
// ---------------------------------------------------------------------------
|
|
303
218
|
// Root path (/) — special handling
|
|
304
219
|
// ---------------------------------------------------------------------------
|
|
305
|
-
describe("root path
|
|
306
|
-
it("
|
|
220
|
+
describe("root path", () => {
|
|
221
|
+
it("passes GET / through with CSP (auth handled client-side)", async () => {
|
|
307
222
|
const req = buildRequest("/");
|
|
308
223
|
const res = await middleware(req);
|
|
309
224
|
expect(isPassThrough(res)).toBe(true);
|
|
225
|
+
expect(res.headers.get("content-security-policy")).not.toBeNull();
|
|
310
226
|
});
|
|
311
227
|
|
|
312
|
-
it("
|
|
313
|
-
const req = buildRequest("/", {
|
|
314
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
315
|
-
});
|
|
316
|
-
const res = await middleware(req);
|
|
317
|
-
expect(isRedirect(res)).toBe(true);
|
|
318
|
-
expect(redirectPath(res)).toBe("/marketplace");
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("redirects authenticated GET / to app domain when NEXT_PUBLIC_APP_DOMAIN is set and host is the marketing domain", async () => {
|
|
322
|
-
process.env.NEXT_PUBLIC_APP_DOMAIN = "app.localhost";
|
|
323
|
-
const req = buildRequest("/", {
|
|
324
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
325
|
-
host: "localhost",
|
|
326
|
-
});
|
|
327
|
-
const res = await middleware(req);
|
|
328
|
-
expect(isRedirect(res)).toBe(true);
|
|
329
|
-
const loc = res.headers.get("location");
|
|
330
|
-
expect(loc).toBe("https://app.localhost/marketplace");
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it("redirects authenticated GET / to /marketplace when already on app subdomain", async () => {
|
|
334
|
-
process.env.NEXT_PUBLIC_APP_DOMAIN = "app.localhost";
|
|
228
|
+
it("passes authenticated GET / through with CSP", async () => {
|
|
335
229
|
const req = buildRequest("/", {
|
|
336
230
|
cookies: { "better-auth.session_token": "valid-token" },
|
|
337
|
-
host: "app.localhost",
|
|
338
231
|
});
|
|
339
232
|
const res = await middleware(req);
|
|
340
|
-
expect(
|
|
341
|
-
expect(redirectPath(res)).toBe("/marketplace");
|
|
233
|
+
expect(isPassThrough(res)).toBe(true);
|
|
342
234
|
});
|
|
343
235
|
});
|
|
344
236
|
|
|
@@ -370,11 +262,10 @@ describe("middleware", () => {
|
|
|
370
262
|
expect(isPassThrough(res)).toBe(true);
|
|
371
263
|
});
|
|
372
264
|
|
|
373
|
-
it("
|
|
265
|
+
it("passes /api/something through (auth handled client-side)", async () => {
|
|
374
266
|
const req = buildRequest("/api/something");
|
|
375
267
|
const res = await middleware(req);
|
|
376
|
-
expect(
|
|
377
|
-
expect(redirectPath(res)).toContain("/login");
|
|
268
|
+
expect(isPassThrough(res)).toBe(true);
|
|
378
269
|
});
|
|
379
270
|
|
|
380
271
|
it("passes /api/something through with a valid session cookie", async () => {
|
|
@@ -393,81 +284,20 @@ describe("middleware", () => {
|
|
|
393
284
|
});
|
|
394
285
|
|
|
395
286
|
// ---------------------------------------------------------------------------
|
|
396
|
-
//
|
|
287
|
+
// All routes pass through (auth is handled client-side by useRequireAuth)
|
|
397
288
|
// ---------------------------------------------------------------------------
|
|
398
|
-
describe("
|
|
399
|
-
it("
|
|
289
|
+
describe("all routes pass through with CSP", () => {
|
|
290
|
+
it("passes /admin through (auth handled client-side)", async () => {
|
|
400
291
|
const req = buildRequest("/admin");
|
|
401
292
|
const res = await middleware(req);
|
|
402
|
-
expect(isRedirect(res)).toBe(true);
|
|
403
|
-
expect(redirectPath(res)).toContain("/login");
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it("redirects authenticated non-admin to /marketplace when accessing /admin", async () => {
|
|
407
|
-
vi.stubGlobal(
|
|
408
|
-
"fetch",
|
|
409
|
-
vi.fn().mockResolvedValue({
|
|
410
|
-
ok: true,
|
|
411
|
-
json: async () => ({ user: { role: "user" } }),
|
|
412
|
-
}),
|
|
413
|
-
);
|
|
414
|
-
const req = buildRequest("/admin", {
|
|
415
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
416
|
-
});
|
|
417
|
-
const res = await middleware(req);
|
|
418
|
-
expect(isRedirect(res)).toBe(true);
|
|
419
|
-
expect(redirectPath(res)).toBe("/marketplace");
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it("allows platform_admin through to /admin", async () => {
|
|
423
|
-
vi.stubGlobal(
|
|
424
|
-
"fetch",
|
|
425
|
-
vi.fn().mockResolvedValue({
|
|
426
|
-
ok: true,
|
|
427
|
-
json: async () => ({ user: { role: "platform_admin" } }),
|
|
428
|
-
}),
|
|
429
|
-
);
|
|
430
|
-
const req = buildRequest("/admin", {
|
|
431
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
432
|
-
});
|
|
433
|
-
const res = await middleware(req);
|
|
434
293
|
expect(isPassThrough(res)).toBe(true);
|
|
294
|
+
expect(res.headers.get("content-security-policy")).not.toBeNull();
|
|
435
295
|
});
|
|
436
296
|
|
|
437
|
-
it("
|
|
438
|
-
|
|
439
|
-
"fetch",
|
|
440
|
-
vi.fn().mockResolvedValue({
|
|
441
|
-
ok: true,
|
|
442
|
-
json: async () => ({ user: { role: "user" } }),
|
|
443
|
-
}),
|
|
444
|
-
);
|
|
445
|
-
const req = buildRequest("/admin/users", {
|
|
446
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
447
|
-
});
|
|
448
|
-
const res = await middleware(req);
|
|
449
|
-
expect(isRedirect(res)).toBe(true);
|
|
450
|
-
expect(redirectPath(res)).toBe("/marketplace");
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it("redirects to /marketplace when session fetch fails (fail closed)", async () => {
|
|
454
|
-
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network error")));
|
|
455
|
-
const req = buildRequest("/admin", {
|
|
456
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
457
|
-
});
|
|
297
|
+
it("passes /admin/users through", async () => {
|
|
298
|
+
const req = buildRequest("/admin/users");
|
|
458
299
|
const res = await middleware(req);
|
|
459
|
-
expect(
|
|
460
|
-
expect(redirectPath(res)).toBe("/marketplace");
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it("does not call fetch for non-admin routes", async () => {
|
|
464
|
-
const fetchSpy = vi.fn().mockResolvedValue({ ok: false });
|
|
465
|
-
vi.stubGlobal("fetch", fetchSpy);
|
|
466
|
-
const req = buildRequest("/marketplace", {
|
|
467
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
468
|
-
});
|
|
469
|
-
await middleware(req);
|
|
470
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
300
|
+
expect(isPassThrough(res)).toBe(true);
|
|
471
301
|
});
|
|
472
302
|
});
|
|
473
303
|
|
|
@@ -660,8 +490,8 @@ describe("CSP nonce in middleware", () => {
|
|
|
660
490
|
expect(csp).toContain("default-src 'self'");
|
|
661
491
|
expect(csp).toMatch(/style-src-elem 'self' 'unsafe-inline' 'nonce-[A-Za-z0-9+/=_-]+'/);
|
|
662
492
|
|
|
663
|
-
expect(csp).toContain("img-src 'self' data: blob:");
|
|
664
|
-
expect(csp).toContain("frame-src https://js.stripe.com");
|
|
493
|
+
expect(csp).toContain("img-src 'self' data: blob: https:");
|
|
494
|
+
expect(csp).toContain("frame-src 'self' https://js.stripe.com");
|
|
665
495
|
expect(csp).toContain("frame-ancestors 'none'");
|
|
666
496
|
expect(csp).toContain("object-src 'none'");
|
|
667
497
|
});
|
|
@@ -677,23 +507,6 @@ describe("CSP nonce in middleware", () => {
|
|
|
677
507
|
const res = await middleware(req);
|
|
678
508
|
expect(res.headers.get("vary")).toBe("*");
|
|
679
509
|
});
|
|
680
|
-
|
|
681
|
-
it("does not overwrite stricter Cache-Control on admin routes", async () => {
|
|
682
|
-
vi.stubGlobal(
|
|
683
|
-
"fetch",
|
|
684
|
-
vi
|
|
685
|
-
.fn()
|
|
686
|
-
.mockResolvedValue(
|
|
687
|
-
new Response(JSON.stringify({ user: { role: "platform_admin" } }), { status: 200 }),
|
|
688
|
-
),
|
|
689
|
-
);
|
|
690
|
-
const req = buildRequest("/admin/dashboard", {
|
|
691
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
692
|
-
});
|
|
693
|
-
const res = await middleware(req);
|
|
694
|
-
// Admin route sets stricter cache headers; withCsp should not overwrite
|
|
695
|
-
expect(res.headers.get("cache-control")).toBe("no-store, no-cache, must-revalidate");
|
|
696
|
-
});
|
|
697
510
|
});
|
|
698
511
|
|
|
699
512
|
// ---------------------------------------------------------------------------
|
|
@@ -11,9 +11,7 @@ describe("next.config headers", () => {
|
|
|
11
11
|
expect(headers).toHaveLength(1);
|
|
12
12
|
expect(headers[0].source).toBe("/:path*");
|
|
13
13
|
|
|
14
|
-
const headerMap = new Map(
|
|
15
|
-
headers[0].headers.map((h: { key: string; value: string }) => [h.key, h.value]),
|
|
16
|
-
);
|
|
14
|
+
const headerMap = new Map(headers[0].headers.map((h: { key: string; value: string }) => [h.key, h.value]));
|
|
17
15
|
expect(headerMap.get("X-Frame-Options")).toBe("DENY");
|
|
18
16
|
expect(headerMap.get("X-Content-Type-Options")).toBe("nosniff");
|
|
19
17
|
// CSP is now set exclusively by middleware (nonce-based), not next.config.ts
|