Pulse/internal/api/code_standards_test.go
2026-04-09 20:15:17 +01:00

471 lines
17 KiB
Go

package api
// Code standards tests: these act as linter rules that run in CI.
// They scan source files for anti-patterns that indicate DRY violations
// and fail the build if someone reintroduces a consolidated pattern.
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// readGoFiles returns the contents of all non-test .go files in the api package directory.
func readGoFiles(t *testing.T) map[string]string {
t.Helper()
files := make(map[string]string)
entries, err := os.ReadDir(".")
if err != nil {
t.Fatalf("failed to read api directory: %v", err)
}
for _, entry := range entries {
name := entry.Name()
if filepath.Ext(name) != ".go" || strings.HasSuffix(name, "_test.go") {
continue
}
data, err := os.ReadFile(name)
if err != nil {
t.Fatalf("failed to read %s: %v", name, err)
}
files[name] = string(data)
}
return files
}
// TestNoInline402Responses ensures handlers use WriteLicenseRequired() instead of
// manually writing HTTP 402 responses. The only files allowed to write 402 directly
// are license_handlers.go and middleware_license.go (which define the helpers).
func TestNoInline402Responses(t *testing.T) {
allowedFiles := map[string]bool{
"license_handlers.go": true,
"middleware_license.go": true,
}
// Match patterns like: WriteHeader(402), WriteHeader(http.StatusPaymentRequired),
// http.Error(...402...), StatusPaymentRequired (used in a write context)
inline402 := regexp.MustCompile(`WriteHeader\(\s*(402|http\.StatusPaymentRequired)\s*\)`)
for name, content := range readGoFiles(t) {
if allowedFiles[name] {
continue
}
if matches := inline402.FindAllStringIndex(content, -1); len(matches) > 0 {
for _, m := range matches {
line := 1 + strings.Count(content[:m[0]], "\n")
t.Errorf("%s:%d: inline 402 response — use WriteLicenseRequired() from license_handlers.go instead", name, line)
}
}
}
}
// TestNoAgentHandlerMethodRedefinition ensures that SetMonitor, SetMultiTenantMonitor,
// and getMonitor are only defined on baseAgentHandlers, not on concrete handler types.
func TestNoAgentHandlerMethodRedefinition(t *testing.T) {
// These methods should only be defined in agent_handlers_base.go
patterns := []struct {
re *regexp.Regexp
name string
}{
{regexp.MustCompile(`func \(h \*(?:Docker|Kubernetes|Host)AgentHandlers\) SetMonitor\b`), "SetMonitor"},
{regexp.MustCompile(`func \(h \*(?:Docker|Kubernetes|Host)AgentHandlers\) SetMultiTenantMonitor\b`), "SetMultiTenantMonitor"},
{regexp.MustCompile(`func \(h \*(?:Docker|Kubernetes|Host)AgentHandlers\) getMonitor\b`), "getMonitor"},
}
for name, content := range readGoFiles(t) {
if name == "agent_handlers_base.go" {
continue
}
for _, p := range patterns {
if loc := p.re.FindStringIndex(content); loc != nil {
line := 1 + strings.Count(content[:loc[0]], "\n")
t.Errorf("%s:%d: %s() redefined on concrete handler type — it's inherited from baseAgentHandlers", name, line, p.name)
}
}
}
}
// ---------------------------------------------------------------------------
// Repo Boundary Audit — Paid-Domain Code in Public Paths
// ---------------------------------------------------------------------------
//
// The pulse public repo should contain only community/core runtime code.
// Paid-domain implementations (RBAC, Audit, SSO, Reporting) are migrating to
// pulse-enterprise behind pkg/extensions contracts.
//
// This test tracks paid-domain implementation files still in internal/api/.
// The list acts as a monotonic ratchet: it can only shrink as files migrate.
// Adding new paid-domain implementation files to internal/api/ will fail CI.
//
// Allowed patterns in the public repo:
// - pkg/extensions/* — interface contracts (imported by enterprise)
// - enterprise_extension_* — binder registration (1 SetXxx func per file)
// - pkg/auth/* — shared auth primitives (also used by core)
// - pkg/audit/* — shared audit primitives (also used by core)
//
// NOT allowed (should migrate to pulse-enterprise over time):
// - Handler implementations that serve paid-only HTTP endpoints
// - Service implementations that execute paid-only business logic
//
// See: pulse-enterprise/docs/V6_REPO_REALIGNMENT.md
// paidDomainFiles is the set of non-test .go files in internal/api/ that
// contain paid-domain implementation code. Each entry is a file that should
// eventually migrate to pulse-enterprise.
//
// Last audited: 2026-02-27 (14 files).
var paidDomainFiles = map[string]string{
// RBAC handlers (5 files)
"access_control_handlers.go": "RBAC",
"access_admin_handlers.go": "RBAC",
"access_admin_recovery.go": "RBAC",
"access_metrics_handlers.go": "RBAC",
"access_tenant_provider.go": "RBAC",
// Audit handlers (1 file)
"activity_audit_handlers.go": "Audit",
// SSO/OIDC/SAML handlers and services (5 files)
"identity_sso_handlers.go": "SSO",
"oidc_handlers.go": "SSO",
"oidc_service.go": "SSO",
"saml_handlers.go": "SSO",
"saml_service.go": "SSO",
// Reporting handlers (2 files)
"metrics_reporting_handlers.go": "Reporting",
"reporting_runtime_snapshot.go": "Reporting",
}
const paidDomainFileCeiling = 13
var knownSharedInfrastructureFiles = map[string]bool{
// Shared SSO outbound helpers centralize restricted fetch/file access and are
// consumed by public handlers; they are not paid-domain business logic.
"sso_outbound.go": true,
}
// TestPaidDomainBoundaryAudit enforces two rules:
//
// 1. No NEW paid-domain implementation files may be added to internal/api/.
// New paid-domain code must go in pulse-enterprise behind pkg/extensions.
// 2. The total count of tracked files can only decrease (migration ratchet).
//
// When a file is migrated to pulse-enterprise, remove it from paidDomainFiles
// and lower paidDomainFileCeiling accordingly.
func TestPaidDomainBoundaryAudit(t *testing.T) {
goFiles := readGoFiles(t) // non-test .go files in internal/api/
// Verify every tracked file still exists (catch stale entries).
for name, domain := range paidDomainFiles {
if _, ok := goFiles[name]; !ok {
t.Errorf("paid-domain file %s (%s) is tracked but no longer exists — remove it from paidDomainFiles and lower paidDomainFileCeiling", name, domain)
}
}
// Count tracked files that exist.
existingCount := 0
for name := range paidDomainFiles {
if _, ok := goFiles[name]; ok {
existingCount++
}
}
// Ratchet: total must not exceed ceiling.
if existingCount > paidDomainFileCeiling {
t.Errorf("paid-domain file count %d exceeds ceiling %d — new paid-domain files must go in pulse-enterprise", existingCount, paidDomainFileCeiling)
}
if existingCount < paidDomainFileCeiling {
t.Logf("paid-domain file count %d is below ceiling %d — lower paidDomainFileCeiling to lock in this improvement", existingCount, paidDomainFileCeiling)
}
// Detect untracked paid-domain files by matching known naming patterns.
// These patterns identify files that implement paid-feature HTTP handlers
// or business logic (not binders, not tests, not shared infrastructure).
paidPatterns := []*regexp.Regexp{
regexp.MustCompile(`^access_`), // RBAC handlers (access_*)
regexp.MustCompile(`^rbac_`), // RBAC handlers (rbac_*)
regexp.MustCompile(`^activity_audit`), // Audit handlers
regexp.MustCompile(`^audit_`), // Audit handlers (audit_*)
regexp.MustCompile(`^(?:oidc|saml)_`), // SSO protocol impls
regexp.MustCompile(`^identity_sso`), // SSO identity handlers
regexp.MustCompile(`^sso_`), // SSO handlers (sso_*)
regexp.MustCompile(`^(?:metrics_)?reporting`), // Reporting handlers
regexp.MustCompile(`^reporting_runtime`), // Reporting runtime
}
// Enterprise extension binder files are the correct pattern — they
// contain only SetXxx registration functions, not implementations.
// Use an exact allowlist to prevent new paid implementations from
// hiding behind the enterprise_extension_ prefix.
knownBinderFiles := map[string]bool{
"enterprise_extension_ai_alert_analysis.go": true,
"enterprise_extension_ai_autofix.go": true,
"enterprise_extension_ai_investigation.go": true,
"enterprise_extension_audit_admin.go": true,
"enterprise_extension_rbac_admin.go": true,
"enterprise_extension_reporting_admin.go": true,
"enterprise_extension_sso_admin.go": true,
}
// These reporting files are intentionally public shared surfaces rather than
// paid-domain implementations: the catalog route is public product metadata,
// and the VM inventory export handler is the default shared implementation
// behind the reporting admin extension boundary.
knownPublicReportingFiles := map[string]bool{
"reporting_catalog_handlers.go": true,
"reporting_inventory_handlers.go": true,
}
for name := range goFiles {
// Skip already-tracked files.
if _, tracked := paidDomainFiles[name]; tracked {
continue
}
// Skip known binder files (exact allowlist, not prefix).
if knownBinderFiles[name] {
continue
}
if knownPublicReportingFiles[name] {
continue
}
if knownSharedInfrastructureFiles[name] {
continue
}
// Flag unknown enterprise_extension_ files (not in the exact allowlist).
if strings.HasPrefix(name, "enterprise_extension_") {
t.Errorf("unknown enterprise extension binder %s — add it to knownBinderFiles if it is a valid binder, or move implementation code to pulse-enterprise", name)
continue
}
// Check against paid-domain patterns.
for _, re := range paidPatterns {
if re.MatchString(name) {
t.Errorf("untracked paid-domain file %s matches pattern %s — new paid-domain implementations must go in pulse-enterprise (if this is a false positive, add an exemption)", name, re.String())
break
}
}
}
}
// TestNoRawBroadcastStateInAgentHandlers ensures agent handler files use
// h.broadcastState(ctx) instead of the raw go h.wsHub.BroadcastState(...) pattern.
func TestNoRawBroadcastStateInAgentHandlers(t *testing.T) {
agentFiles := map[string]bool{
"docker_agents.go": true,
"kubernetes_agents.go": true,
"agent_ingest.go": true,
}
raw := regexp.MustCompile(`go h\.wsHub\.BroadcastState\(`)
for name, content := range readGoFiles(t) {
if !agentFiles[name] {
continue
}
if matches := raw.FindAllStringIndex(content, -1); len(matches) > 0 {
for _, m := range matches {
line := 1 + strings.Count(content[:m[0]], "\n")
t.Errorf("%s:%d: raw BroadcastState call — use h.broadcastState(r.Context()) instead", name, line)
}
}
}
}
// TestNoRawHTTPErrorInSettingsHandlers ensures settings handler files use
// writeErrorResponse() instead of http.Error(). Raw http.Error() returns
// plain text bodies that break frontend JSON parsing during first-session
// error states, causing console errors and broken UX.
//
// Settings handlers must return structured JSON via writeErrorResponse()
// so the frontend can parse error codes and show appropriate error UI.
func TestNoRawHTTPErrorInSettingsHandlers(t *testing.T) {
settingsFiles := map[string]bool{
"system_settings.go": true,
"subscription_entitlements.go": true,
}
rawHTTPError := regexp.MustCompile(`http\.Error\(`)
for name, content := range readGoFiles(t) {
if !settingsFiles[name] {
continue
}
if matches := rawHTTPError.FindAllStringIndex(content, -1); len(matches) > 0 {
for _, m := range matches {
line := 1 + strings.Count(content[:m[0]], "\n")
t.Errorf("%s:%d: raw http.Error() call — use writeErrorResponse() for structured JSON errors", name, line)
}
}
}
}
// TestPkgLicensingImportBoundary ensures that only licensing_bridge.go may
// import pkg/licensing directly. All other files in internal/api/ must use
// bridge wrappers (type aliases, constants, and functions defined in
// licensing_bridge.go) instead of importing pkg/licensing directly.
//
// This prevents tight coupling between business logic handlers and the
// licensing package, keeping the boundary clean and auditable.
func TestPkgLicensingImportBoundary(t *testing.T) {
// Only licensing_bridge.go is allowed to import pkg/licensing.
allowedFiles := map[string]bool{
"licensing_bridge.go": true,
}
importPattern := regexp.MustCompile(`"github\.com/rcourtman/pulse-go-rewrite/pkg/licensing(?:/[^"]*)?"\s*$`)
for name, content := range readGoFiles(t) {
if allowedFiles[name] {
continue
}
for i, line := range strings.Split(content, "\n") {
if importPattern.MatchString(line) {
t.Errorf("%s:%d: direct pkg/licensing import — use bridge wrappers from licensing_bridge.go instead", name, i+1)
}
}
}
}
// ---------------------------------------------------------------------------
// Phase 3 Governance — Repo Realignment Boundary Locks
// ---------------------------------------------------------------------------
//
// These tests enforce Phase 3 of the repo realignment plan
// (pulse-enterprise/docs/V6_REPO_REALIGNMENT.md):
//
// "Lock branch/release gates: no paid implementation changes merged to
// pulse after cut-over."
//
// The tests below are the Go-native equivalents of the enforcement modes
// in scripts/audit-private-boundary.sh, ensuring these rules run as part
// of `go test ./...` without requiring a separate CI script step.
// readConsumerGoFilesFromDir returns the contents of all non-test .go files
// in the given directory (relative to internal/api/).
func readConsumerGoFilesFromDir(t *testing.T, relDir string) map[string]string {
t.Helper()
dir := filepath.Join("..", relDir)
files := make(map[string]string)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
return nil
}
data, readErr := os.ReadFile(path)
if readErr != nil {
t.Fatalf("failed to read %s: %v", path, readErr)
}
files[path] = string(data)
return nil
})
if err != nil {
t.Fatalf("failed to walk %s: %v", relDir, err)
}
return files
}
// TestNoInternalLicenseImportFromAPIProd ensures that no production file in
// internal/api/ directly imports internal/license (root or sub-packages).
// All license access must go through pkg/licensing via licensing_bridge.go.
//
// This is the Go-native equivalent of audit-private-boundary.sh
// --enforce-api-imports.
func TestNoInternalLicenseImportFromAPIProd(t *testing.T) {
importPattern := regexp.MustCompile(`"github\.com/rcourtman/pulse-go-rewrite/internal/license(?:/[^"]*)?"\s*`)
for name, content := range readGoFiles(t) {
for i, line := range strings.Split(content, "\n") {
if importPattern.MatchString(line) {
t.Errorf("%s:%d: direct internal/license import — use pkg/licensing bridge wrappers instead", name, i+1)
}
}
}
}
// TestNoInternalLicenseImportFromConsumerPackages ensures that no production
// file outside internal/api/ and internal/license/ imports internal/license
// (root package). Consumer packages must use pkg/licensing contracts.
//
// Scans all internal/ subdirectories (excluding api/ and license/), plus
// cmd/ and pkg/ at the repo root. This covers all runtime Go code; the
// shell-based audit-private-boundary.sh --enforce-nonapi-imports provides
// broader repo-wide coverage including non-runtime paths.
func TestNoInternalLicenseImportFromConsumerPackages(t *testing.T) {
// Exempt directories under internal/:
// api — covered by TestNoInternalLicenseImportFromAPIProd
// license — intra-package imports are fine
exempt := map[string]bool{
"api": true,
"license": true,
}
importPattern := regexp.MustCompile(`"github\.com/rcourtman/pulse-go-rewrite/internal/license"\s*`)
// Scan all internal/ subdirectories (dynamically discovered).
internalDir := filepath.Join("..")
entries, err := os.ReadDir(internalDir)
if err != nil {
t.Fatalf("failed to read internal/ directory: %v", err)
}
for _, entry := range entries {
if !entry.IsDir() || exempt[entry.Name()] {
continue
}
for path, content := range readConsumerGoFilesFromDir(t, entry.Name()) {
for i, line := range strings.Split(content, "\n") {
if importPattern.MatchString(line) {
t.Errorf("%s:%d: direct internal/license import — consumer packages must use pkg/licensing contracts", path, i+1)
}
}
}
}
// Also scan cmd/ and pkg/ at the repo root.
repoRoot := filepath.Join("..", "..")
for _, topDir := range []string{"cmd", "pkg"} {
dir := filepath.Join(repoRoot, topDir)
if _, statErr := os.Stat(dir); os.IsNotExist(statErr) {
continue
}
files := make(map[string]string)
walkErr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
return nil
}
data, readErr := os.ReadFile(path)
if readErr != nil {
t.Fatalf("failed to read %s: %v", path, readErr)
}
files[path] = string(data)
return nil
})
if walkErr != nil {
t.Fatalf("failed to walk %s: %v", topDir, walkErr)
}
for path, content := range files {
for i, line := range strings.Split(content, "\n") {
if importPattern.MatchString(line) {
t.Errorf("%s:%d: direct internal/license import — use pkg/licensing contracts instead", path, i+1)
}
}
}
}
}