check-safety/main_test.go
Alex Dunmow 0749c22d30 feat(2c): forbid all first-party imports in standalone plugins; fix --plugin-dir-only scans
Import boundary now covers the whole first-party tree, not just block/cms:
block/core and block/orchestrator imports are violations too, with an
explicit carve-out for the packages that deliberately stayed core after
the pluginsdk extraction (core/captcha, core/backup — calcomblock uses
captcha today). The .templ import scan gets the same rule.

defaultScanTargetDir: --plugin-dir with no positional target used to
leave targetDir at cwd, so the checker scanned its own repo and
self-reported failures while never scanning the plugin. It now targets
the first plugin root, matching the positional form.

Verified green across cms + all 11 v0.2.1 fleet repos (bidmasters still
fails the version anchor as expected — it pins v0.2.0 on a detached
HEAD, unrelated to these changes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:48:01 +08:00

277 lines
9.0 KiB
Go

package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestDefaultScanTargetDirUsesCWDPlugin(t *testing.T) {
cwd := t.TempDir()
pluginMod := filepath.Join(cwd, "plugin.mod")
if err := os.WriteFile(pluginMod, []byte("[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n"), 0644); err != nil {
t.Fatalf("write plugin.mod: %v", err)
}
got := defaultScanTargetDir(".", nil, cwd)
if got != cwd {
t.Fatalf("defaultScanTargetDir() = %q, want %q", got, cwd)
}
}
func TestDefaultScanTargetDirUsesPluginRootWhenOnlyPluginDirGiven(t *testing.T) {
cwd := t.TempDir() // no plugin.mod — simulates running from the check-safety repo itself
pluginRoot := t.TempDir()
got := defaultScanTargetDir(".", []string{pluginRoot}, cwd)
if got != pluginRoot {
t.Fatalf("defaultScanTargetDir() = %q, want plugin root %q", got, pluginRoot)
}
}
func TestDefaultScanTargetDirKeepsExplicitTargetOverPluginRoots(t *testing.T) {
cwd := t.TempDir()
pluginRoot := t.TempDir()
got := defaultScanTargetDir("/some/backend", []string{pluginRoot}, cwd)
if got != "/some/backend" {
t.Fatalf("defaultScanTargetDir() = %q, want explicit target /some/backend", got)
}
}
func TestDefaultScanTargetDirFallsBackToCoreBackend(t *testing.T) {
cwd := t.TempDir()
got := defaultScanTargetDir(".", nil, cwd)
want := filepath.Join(blockNinjaRepoRoot(), "backend")
if got != want {
t.Fatalf("defaultScanTargetDir() = %q, want %q", got, want)
}
}
func TestShouldIncludeCoreTargetsUsesBackendPathAsToolHostForPluginScans(t *testing.T) {
targetDir := filepath.Join(blockNinjaRepoRoot(), "backend")
if !shouldIncludeCoreTargets(targetDir, targetDir, nil, nil) {
t.Fatalf("expected core targets to be included when no plugin flags are present")
}
if shouldIncludeCoreTargets(targetDir, targetDir, []string{"/tmp/plugin"}, nil) {
t.Fatalf("expected core targets to be skipped when plugin roots are passed with the checker backend path")
}
if shouldIncludeCoreTargets(targetDir, targetDir, nil, []string{"/tmp/plugin/web/src"}) {
t.Fatalf("expected core targets to be skipped when plugin pages are passed with the checker backend path")
}
}
func TestCollectTargetsSkipsCoreWhenIncludeCoreTargetsFalse(t *testing.T) {
repoRoot := t.TempDir()
backendDir := filepath.Join(repoRoot, "backend")
pluginFrontendDir := filepath.Join(repoRoot, "plugins", "example", "web", "src")
for _, dir := range []string{backendDir, pluginFrontendDir} {
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
}
if targets := collectBackendScanTargets(repoRoot, backendDir, false); len(targets) != 0 {
t.Fatalf("collectBackendScanTargets(..., false) returned %d targets, want 0", len(targets))
}
frontendTargets := collectFrontendScanTargets(repoRoot, backendDir, []pluginScanTarget{{
root: filepath.Join(repoRoot, "plugins", "example"),
frontendDir: pluginFrontendDir,
display: "plugins/example",
frontendLabel: "plugins/example/web/src",
}}, false)
if len(frontendTargets) != 1 {
t.Fatalf("collectFrontendScanTargets(..., false) returned %d targets, want 1", len(frontendTargets))
}
if frontendTargets[0].dir != pluginFrontendDir {
t.Fatalf("frontend target dir = %q, want %q", frontendTargets[0].dir, pluginFrontendDir)
}
if !frontendTargets[0].pluginRules {
t.Fatalf("frontend target pluginRules = false, want true")
}
if !frontendTargets[0].managedConfigs {
t.Fatalf("frontend target managedConfigs = false, want true")
}
}
func TestResolveScanRootsDetectsConsolidatedBackendDir(t *testing.T) {
repoRoot := t.TempDir()
backendDir := filepath.Join(repoRoot, "backend")
protoDir := filepath.Join(repoRoot, "proto")
for _, dir := range []string{backendDir, protoDir} {
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
}
if err := os.WriteFile(filepath.Join(backendDir, "go.mod"), []byte("module example.com/cms/backend\n"), 0644); err != nil {
t.Fatalf("write go.mod: %v", err)
}
if err := os.WriteFile(filepath.Join(repoRoot, "buf.yaml"), []byte("version: v2\n"), 0644); err != nil {
t.Fatalf("write buf.yaml: %v", err)
}
gotBackend, gotRepo, err := resolveScanRoots(backendDir)
if err != nil {
t.Fatalf("resolveScanRoots() error = %v", err)
}
if gotBackend != backendDir {
t.Fatalf("backendDir = %q, want %q", gotBackend, backendDir)
}
if gotRepo != repoRoot {
t.Fatalf("repoRoot = %q, want %q", gotRepo, repoRoot)
}
}
func TestDiscoverOrchestratorTestTargetsRequiresCoreScan(t *testing.T) {
repoRoot := blockNinjaRepoRoot()
backendDir := filepath.Join(repoRoot, "backend")
if targets := discoverOrchestratorTestTargets(repoRoot, backendDir, false); len(targets) != 0 {
t.Fatalf("discoverOrchestratorTestTargets(..., false) returned %d targets, want 0", len(targets))
}
pluginRoot := filepath.Join(t.TempDir(), "plugin")
if targets := discoverOrchestratorTestTargets(pluginRoot, pluginRoot, true); len(targets) != 0 {
t.Fatalf("discoverOrchestratorTestTargets(plugin root) returned %d targets, want 0", len(targets))
}
}
func TestDiscoverOrchestratorTestTargetsFromRoot(t *testing.T) {
root := t.TempDir()
backendDir := filepath.Join(root, "backend")
if err := os.MkdirAll(backendDir, 0755); err != nil {
t.Fatalf("mkdir backend: %v", err)
}
if targets := discoverOrchestratorTestTargetsFromRoot(root); len(targets) != 0 {
t.Fatalf("targets without go.mod = %d, want 0", len(targets))
}
if err := os.WriteFile(filepath.Join(backendDir, "go.mod"), []byte("module example.com/orchestrator\n"), 0644); err != nil {
t.Fatalf("write go.mod: %v", err)
}
targets := discoverOrchestratorTestTargetsFromRoot(root)
if len(targets) != 1 {
t.Fatalf("targets = %d, want 1", len(targets))
}
if targets[0].label != "orchestrator/backend" {
t.Fatalf("label = %q, want orchestrator/backend", targets[0].label)
}
if targets[0].moduleRoot != backendDir {
t.Fatalf("moduleRoot = %q, want %q", targets[0].moduleRoot, backendDir)
}
if got := strings.Join(targets[0].args, " "); got != "test ./..." {
t.Fatalf("args = %q, want test ./...", got)
}
}
func TestResolvePluginTargetsUsesRootFolderNameForDisplay(t *testing.T) {
repoRoot := t.TempDir()
pluginRoot := filepath.Join(t.TempDir(), "messenger")
pluginFrontend := filepath.Join(pluginRoot, "web")
for _, dir := range []string{pluginRoot, pluginFrontend} {
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
}
if err := os.WriteFile(filepath.Join(pluginFrontend, "index.tsx"), []byte("export const x = 1\n"), 0644); err != nil {
t.Fatalf("write frontend file: %v", err)
}
targets, unresolved := resolvePluginTargets(repoRoot, []string{pluginRoot})
if len(unresolved) != 0 {
t.Fatalf("unresolved = %v, want none", unresolved)
}
if len(targets) != 1 {
t.Fatalf("targets = %d, want 1", len(targets))
}
if targets[0].display != "messenger" {
t.Fatalf("display = %q, want messenger", targets[0].display)
}
if targets[0].frontendLabel != "messenger" {
t.Fatalf("frontendLabel = %q, want messenger", targets[0].frontendLabel)
}
}
func TestExtractRBACMethodRolesParsesRoleValues(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "interceptor.go")
if err := os.WriteFile(file, []byte(`package rbac
type Role string
const (
RoleViewer Role = "viewer"
RoleAdmin Role = "admin"
)
var MethodRoles = map[string]Role{
"/example.v1.Service/Public": "",
"/example.v1.Service/View": RoleViewer,
"/example.v1.Service/Admin": RoleAdmin,
}
`), 0644); err != nil {
t.Fatalf("write interceptor.go: %v", err)
}
methods, err := extractRBACMethodRoles(file)
if err != nil {
t.Fatalf("extractRBACMethodRoles() error = %v", err)
}
if methods["/example.v1.Service/Public"] != "public" {
t.Fatalf("public role = %q, want public", methods["/example.v1.Service/Public"])
}
if methods["/example.v1.Service/View"] != "viewer" {
t.Fatalf("viewer role = %q, want viewer", methods["/example.v1.Service/View"])
}
if methods["/example.v1.Service/Admin"] != "admin" {
t.Fatalf("admin role = %q, want admin", methods["/example.v1.Service/Admin"])
}
}
func TestExtractRBACMethodRolesParsesSeedTable(t *testing.T) {
// The CMS renamed the static RBAC table to methodRolesSeed, kept behind an
// atomic-pointer copy-on-write live table for thread safety. The extractor must
// recognise methodRolesSeed as well as MethodRoles, or every CMS RPC reads as
// "missing RBAC entry".
dir := t.TempDir()
file := filepath.Join(dir, "interceptor.go")
if err := os.WriteFile(file, []byte(`package rbac
type Role string
const (
RoleAdmin Role = "admin"
)
var methodRolesSeed = map[string]Role{
"/example.v1.Service/Public": "",
"/example.v1.Service/Admin": RoleAdmin,
}
`), 0644); err != nil {
t.Fatalf("write interceptor.go: %v", err)
}
methods, err := extractRBACMethodRoles(file)
if err != nil {
t.Fatalf("extractRBACMethodRoles() error = %v", err)
}
if methods["/example.v1.Service/Public"] != "public" {
t.Fatalf("public role = %q, want public", methods["/example.v1.Service/Public"])
}
if methods["/example.v1.Service/Admin"] != "admin" {
t.Fatalf("admin role = %q, want admin", methods["/example.v1.Service/Admin"])
}
}