mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
3c6f9846b5
The usage observer records aggregate outcomes through the existing settings store without implementing a paid renderer or entitlement decision. Declare its exact shared-adapter path so the repository audit can distinguish it from new private reporting implementation. Exercise the real audit against sibling paths and private licensing imports to retain those restrictions for the observer.
486 lines
18 KiB
Go
486 lines
18 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,
|
|
// Availability computation for the default report handlers. Since
|
|
// the enterprise reporting binder delegates generation to these
|
|
// defaults (the enterprise fork silently drifted and was removed),
|
|
// internal/api is the canonical home for the report pipeline.
|
|
"reporting_availability.go": true,
|
|
// Subject identity/alert enrichment for that same default pipeline.
|
|
// This exact helper adds no endpoint, entitlement or private import.
|
|
"reporting_subject_alerts.go": true,
|
|
// Shared tenant/history adapter only. New paid PDF/CSV rendering lives
|
|
// in pulse-enterprise behind GenerateBackupHealthReportFunc.
|
|
"reporting_backup_health.go": true,
|
|
// Shared content-free usage observation for manual and scheduled reports.
|
|
// No renderer, endpoint, entitlement decision or private import.
|
|
"reporting_activity.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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|