package main import ( "os" "path/filepath" "strings" "testing" ) func TestCheckFrontendAllowsDocumentedNonProtoFetches(t *testing.T) { t.Parallel() tests := []struct { name string file string target string }{ { name: "backup multipart upload", file: "components/admin/backups/restore-new-instance-dialog.tsx", target: "/api/push/upload?account_id=${encodeURIComponent(accountId)}", }, { name: "helpdesk multipart upload", file: "components/helpdesk/helpers.ts", target: "/api/helpdesk/upload", }, { name: "AI chat page stream", file: "components/ai-agents/chat-page.tsx", target: "/api/ai/chat/stream", }, { name: "AI chat widget stream", file: "components/ai-agents/chat-widget.tsx", target: "/api/ai/chat/stream", }, { name: "support bug report multipart upload", file: "components/bug-report/bug-report-dialog.tsx", target: "/api/support/bug-reports", }, { name: "support attachment multipart upload", file: "components/support/help-widget.tsx", target: "/api/support/tickets/${ticketId}/messages/${messageId}/attachments", }, { name: "support availability probe", file: "lib/support/availability.ts", target: "/api/support/tickets", }, { name: "MCP device status", file: "routes/admin/authorize-device.tsx", target: "/api/mcp/device/status", }, { name: "MCP device authorization", file: "routes/admin/authorize-device.tsx", target: "/api/mcp/device/authorize", }, { name: "MCP device denial", file: "routes/admin/authorize-device.tsx", target: "/api/mcp/device/deny", }, { name: "plugin multipart upload", file: "routes/admin/plugins.tsx", target: "/api/plugins/upload?token=${encodeURIComponent(token)}", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, filepath.FromSlash(tt.file)) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } source := "const response = await fetch(`" + tt.target + "`, { method: \"POST\" })\n" if err := os.WriteFile(path, []byte(source), 0o644); err != nil { t.Fatal(err) } violations, warnings := checkFrontend(dir) if len(violations) != 0 || len(warnings) != 0 { t.Fatalf("documented REST fetch returned violations=%#v warnings=%#v", violations, warnings) } }) } } func TestCheckFrontendRejectsUndocumentedNonProtoFetches(t *testing.T) { t.Parallel() tests := []struct { name string file string target string }{ { name: "unknown endpoint in an allowed file", file: "routes/admin/authorize-device.tsx", target: "/api/mcp/device/revoke-all", }, { name: "allowed endpoint in an unknown file", file: "routes/admin/unknown.tsx", target: "/api/mcp/device/status", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, filepath.FromSlash(tt.file)) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } source := "const response = await fetch(`" + tt.target + "`)\n" if err := os.WriteFile(path, []byte(source), 0o644); err != nil { t.Fatal(err) } violations, warnings := checkFrontend(dir) if len(warnings) != 0 { t.Fatalf("unexpected warning for undocumented REST fetch: %#v", warnings) } if len(violations) != 1 || violations[0].rule != "no-fetch-api" || !strings.Contains(violations[0].snippet, tt.target) { t.Fatalf("violations = %#v, want one no-fetch-api finding for %q", violations, tt.target) } }) } } func TestAllowedNonProtoFetchesAreNarrowAndDocumented(t *testing.T) { t.Parallel() seen := make(map[string]bool, len(allowedNonProtoFetches)) for _, allowance := range allowedNonProtoFetches { if allowance.file == "" || allowance.target == "" || allowance.reason == "" { t.Fatalf("incomplete non-proto fetch allowance: %#v", allowance) } if strings.HasSuffix(allowance.target, "/") { t.Fatalf("non-proto fetch allowance must name an exact target, got %q", allowance.target) } key := allowance.file + "\x00" + allowance.target if seen[key] { t.Fatalf("duplicate non-proto fetch allowance for %s %s", allowance.file, allowance.target) } seen[key] = true } } func TestCheckPluginPagesFlagsQueryClientProvider(t *testing.T) { dir := t.TempDir() file := filepath.Join(dir, "editor.tsx") if err := os.WriteFile(file, []byte(`import { QueryClientProvider } from '@tanstack/react-query' export function Editor() { return
} `), 0644); err != nil { t.Fatalf("write editor.tsx: %v", err) } violations := checkPluginPages([]string{dir}) found := false for _, v := range violations { if v.rule != "no-queryclientprovider-in-plugin" { continue } if v.snippet != "remove QueryClientProvider; BlockNinja core provides it." { t.Fatalf("snippet = %q, want concise remediation", v.snippet) } found = true break } if !found { t.Fatalf("expected QueryClientProvider violation, got %#v", violations) } } func TestCheckFrontendSkipsTestFiles(t *testing.T) { dir := t.TempDir() // A transport-layer regression test legitimately drives createClient over the // real transport to exercise interceptors — it is not shipped UI. The frontend // pattern checks must skip *.test.* / *.spec.* files, matching the convention // the coming-soon and TODO checks already follow. for _, name := range []string{"transport.test.ts", "auth.test.tsx", "util.spec.ts", "view.spec.tsx"} { if err := os.WriteFile(filepath.Join(dir, name), []byte(`import { createClient } from "@connectrpc/connect"; import axios from "axios"; const client = createClient(AuthService, transport); `), 0644); err != nil { t.Fatalf("write %s: %v", name, err) } } violations, _ := checkFrontend(dir) if len(violations) != 0 { t.Fatalf("expected test/spec files to be skipped, got %d violation(s): %#v", len(violations), violations) } } func TestIsBrowserConfirmCall(t *testing.T) { tests := []struct { lines []string idx int want bool }{ {lines: []string{`if (window.confirm("delete?")) {`}, idx: 0, want: true}, {lines: []string{`if (confirm("delete?")) {`}, idx: 0, want: true}, {lines: []string{`const confirmed = await confirm({ title: "Delete" })`}, idx: 0, want: false}, {lines: []string{`confirm({ title: "Delete" }).then((confirmed) => {`}, idx: 0, want: false}, { lines: []string{ `const confirmed = await`, ` confirm({`, ` title: "Delete",`, ` })`, }, idx: 1, want: false, }, { lines: []string{ `confirm({`, ` title: "Leave",`, `}).then((confirmed) => {`, }, idx: 0, want: false, }, } for _, tt := range tests { if got := isBrowserConfirmCall(tt.lines, tt.idx); got != tt.want { t.Fatalf("isBrowserConfirmCall(%#v, %d) = %v, want %v", tt.lines, tt.idx, got, tt.want) } } } func TestIsRawButtonJSXLine(t *testing.T) { tests := []struct { line string want bool }{ {line: ``, want: true}, {line: ``, want: false}, {line: ``, want: false}, {line: `{/*
) } `), 0o644); err != nil { t.Fatal(err) } violations, _ := checkFrontend(dir) var rawButton []frontendViolation for _, v := range violations { if v.rule == "no-raw-button" { rawButton = append(rawButton, v) } } if len(rawButton) != 1 { t.Fatalf("expected exactly one no-raw-button violation, got %d: %#v", len(rawButton), violations) } if rawButton[0].line != 4 { t.Fatalf("violation line = %d, want 4", rawButton[0].line) } } func TestCheckFrontendRawButtonExemptions(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, "components", "ui"), 0o755); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(dir, "lib"), 0o755); err != nil { t.Fatal(err) } // The Button primitive itself renders a raw `\n"), 0o644); err != nil { t.Fatal(err) } violations, _ := checkFrontend(dir) for _, v := range violations { if v.rule == "no-raw-button" { t.Fatalf("expected no no-raw-button violations, got %#v", v) } } } func TestCheckPluginPagesFlagsRawButton(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "settings.tsx"), []byte(`export function Settings() { return } `), 0o644); err != nil { t.Fatal(err) } violations := checkPluginPages([]string{dir}) found := false for _, v := range violations { if v.rule == "no-raw-button-in-plugin" { found = true break } } if !found { t.Fatalf("expected no-raw-button-in-plugin violation, got %#v", violations) } } func TestPluginRESTFileAllowed(t *testing.T) { if !pluginRESTFileAllowed("/tmp/repo/backend/internal/plugins/calcomblock/web/settings.tsx") { t.Fatalf("expected documented calcom settings panel to be allowlisted") } if !pluginRESTFileAllowed("/tmp/repo/backend/internal/plugins/calcomblock/web/editor.tsx") { t.Fatalf("expected documented calcom editor panel to be allowlisted") } if pluginRESTFileAllowed("/tmp/repo/backend/internal/plugins/calcomblock/web/dashboard.tsx") { t.Fatalf("unexpected allowlist hit for non-allowlisted plugin file") } }