check-safety/main_test.go
Alex Dunmow 73259c6b30 fix(check-safety): recognize methodRolesSeed RBAC table; resolve backend scan-root
extractRBACMethodRoles matched only a map literal named MethodRoles. The CMS now
keeps its RBAC table behind an atomic-pointer copy-on-write live table, with the
static literal renamed to methodRolesSeed -- so the validator read an empty table
and reported 641 phantom "missing RBAC entry" methods, blinding the gate to any
genuine unregistered RPC. Match methodRolesSeed as well as MethodRoles, and add a
regression test (TestExtractRBACMethodRolesParsesSeedTable).

resolveScanRoots: also resolve a `backend` directory containing go.mod whose
parent holds buf.yaml or proto/, so the checker targets that layout correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:04:40 +08:00

257 lines
8.4 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 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"])
}
}