diff --git a/generator/classic/manifest_path_test.go b/generator/classic/manifest_path_test.go new file mode 100644 index 00000000..7541e9bf --- /dev/null +++ b/generator/classic/manifest_path_test.go @@ -0,0 +1,93 @@ +// Copyright 2026, Jamf Software LLC + +package classic + +import ( + "fmt" + "sort" + "strings" + "testing" +) + +// isCleanPathSegment reports whether s is a well-formed single URL path segment +// for the Classic API: non-empty, no whitespace, no slashes (it is one segment, +// not a path), and no control characters. A typo'd token in resources.yaml +// (stray space, leading slash, embedded slash) produces a broken /JSSResource/ +// URL at runtime that nothing else catches — this is the surface that proof guards. +func isCleanPathSegment(s string) bool { + if s == "" { + return false + } + for _, r := range s { + if r <= ' ' || r == '/' || r == '\\' { + return false + } + } + return true +} + +// TestProofClassicManifestPathsWellFormed validates the hand-maintained classic +// manifest. The generated modern paths come straight from OpenAPI specs and are +// tautologically valid, but specs/classic/resources.yaml is hand-edited, so its +// path tokens are a real drift surface. We assert every Path/IDPath/GroupPath +// token is a clean single segment and that the constructed /JSSResource/{Path} +// URL is well-formed. +func TestProofClassicManifestPathsWellFormed(t *testing.T) { + resources, err := ParseManifest("../../specs/classic/resources.yaml") + if err != nil { + t.Fatalf("ParseManifest(real manifest): %v", err) + } + + // Floor sanity check: an empty parse would make "no violations" silent. + const minResources = 10 + if len(resources) < minResources { + t.Fatalf("parsed only %d classic resources (expected >= %d) — manifest or parser regressed", len(resources), minResources) + } + + var violations []string + for _, r := range resources { + label := r.CLIName + if label == "" { + label = r.Name + } + + // A clean Path token guarantees the constructed "/JSSResource/"+Path URL + // is well-formed (no doubled slash, no whitespace), so the segment check + // is the whole check — no separate URL assertion is needed. + if !isCleanPathSegment(r.Path) { + violations = append(violations, fmt.Sprintf("%s: Path token %q is not a clean URL segment", label, r.Path)) + } + // IDPath is always populated by ParseManifest (defaults to "id"); the + // check still validates custom id_path tokens such as "groupid". + if !isCleanPathSegment(r.IDPath) { + violations = append(violations, fmt.Sprintf("%s: IDPath token %q is not a clean URL segment", label, r.IDPath)) + } + // GroupPath is only set for resources with group endpoints. + if r.GroupPath != "" && !isCleanPathSegment(r.GroupPath) { + violations = append(violations, fmt.Sprintf("%s: GroupPath token %q is not a clean URL segment", label, r.GroupPath)) + } + } + + if len(violations) > 0 { + sort.Strings(violations) + t.Errorf("%d malformed classic-manifest path(s) in specs/classic/resources.yaml:\n %s\n\nFix the offending entry's path/id_path/groups_path token in specs/classic/resources.yaml.", + len(violations), strings.Join(violations, "\n ")) + } +} + +// TestIsCleanPathSegment is a permanent guard on the validator itself: the floor +// check above protects against an empty parse, but only this test catches a +// future edit that weakens isCleanPathSegment (e.g. dropping the slash check), +// which would otherwise let the manifest proof silently pass. +func TestIsCleanPathSegment(t *testing.T) { + for _, s := range []string{"", "policies/extra", "policies ", "/policies", "back\\slash"} { + if isCleanPathSegment(s) { + t.Errorf("%q should be rejected", s) + } + } + for _, s := range []string{"policies", "osxconfigurationprofiles", "groupid"} { + if !isCleanPathSegment(s) { + t.Errorf("%q should be accepted", s) + } + } +} diff --git a/internal/commands/proof_short_test.go b/internal/commands/proof_short_test.go new file mode 100644 index 00000000..abec0aa5 --- /dev/null +++ b/internal/commands/proof_short_test.go @@ -0,0 +1,52 @@ +// Copyright 2026, Jamf Software LLC + +package commands + +import ( + "sort" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +// TestProofAllCommandsHaveShort asserts every command in the tree carries a +// non-empty Short description. Short is what `--help`, the generated site +// (`commands -o json`), and agent tooling surface as the one-line summary; an +// empty Short ships a blank, unusable command. Empirically 0 violations today, +// so this is a regression guard: it fails the moment a new hand-written or +// generated command lands without a Short. +func TestProofAllCommandsHaveShort(t *testing.T) { + root := NewRootCmd("test", "abc123", "2024-01-01", "unknown") + + var ( + violations []string + inspected int + ) + // walkCommands does not invoke fn on hidden commands (though it still + // descends into their children) per its contract in + // destructive_annotations_test.go, so a hidden command with an empty Short + // is intentionally not flagged. + walkCommands(root, func(cmd *cobra.Command) { + inspected++ + if strings.TrimSpace(cmd.Short) == "" { + violations = append(violations, commandPath(cmd)) + } + }) + + // Floor sanity check: if the walk inspects almost nothing, the tree wiring + // regressed and a "no violations" pass would be silent. Current tree is + // ~1299 commands; 100 leaves room for churn without masking a major break. + // Fatalf (not Errorf): a broken walk yields an empty violations list too, so + // there is nothing useful to report alongside the floor failure. + const minInspected = 100 + if inspected < minInspected { + t.Fatalf("walked only %d commands (expected >= %d) — command tree wiring likely regressed", inspected, minInspected) + } + + if len(violations) > 0 { + sort.Strings(violations) + t.Errorf("%d command(s) missing a non-empty Short:\n %s\n\nFix by setting Short on each cobra.Command. For generated commands, edit the generator template (generator/parser/generator.go resourceTemplate, generator/classic/generator.go classicResourceTemplate, or generator/platform/template.go resourceTemplate) and run `make generate` — do not edit files under internal/commands/pro/generated/ or internal/commands/platform/generated/.", + len(violations), strings.Join(violations, "\n ")) + } +}