@@ -4,6 +4,9 @@ package policy_test |
| 4 | | 4 | |
| 5 | import ( | 5 | import ( |
| 6 | "context" | 6 | "context" |
| | 7 | + "os" |
| | 8 | + "path/filepath" |
| | 9 | + "strings" |
| 7 | "testing" | 10 | "testing" |
| 8 | | 11 | |
| 9 | "github.com/tenseleyFlow/shithub/internal/auth/policy" | 12 | "github.com/tenseleyFlow/shithub/internal/auth/policy" |
@@ -316,3 +319,30 @@ func TestCacheInvalidate(t *testing.T) { |
| 316 | t.Errorf("after invalidate, admin should deny without role row") | 319 | t.Errorf("after invalidate, admin should deny without role row") |
| 317 | } | 320 | } |
| 318 | } | 321 | } |
| | 322 | + |
| | 323 | + |
| | 324 | +// TestPermissionsDoc_CoversEveryAction guards against drift between |
| | 325 | +// the policy package and docs/internal/permissions.md. The doc's |
| | 326 | +// "Action → minimum role" table must mention every Action constant |
| | 327 | +// exposed via AllActions, so an action added in code without a doc |
| | 328 | +// update fails this test loudly. (S00-S25 audit, finding for doc/code |
| | 329 | +// sync.) |
| | 330 | +// |
| | 331 | +// Matching is by the action's string value (e.g. "repo:read") which |
| | 332 | +// is what the doc renders verbatim in backticks. We do not check the |
| | 333 | +// exact min-role column — that lives in `mirrorMinRoleFor` already. |
| | 334 | +func TestPermissionsDoc_CoversEveryAction(t *testing.T) { |
| | 335 | + t.Parallel() |
| | 336 | + docPath := filepath.Join("..", "..", "..", "docs", "internal", "permissions.md") |
| | 337 | + body, err := os.ReadFile(docPath) |
| | 338 | + if err != nil { |
| | 339 | + t.Fatalf("read permissions.md: %v", err) |
| | 340 | + } |
| | 341 | + doc := string(body) |
| | 342 | + for _, a := range policy.AllActions { |
| | 343 | + needle := "`" + string(a) + "`" |
| | 344 | + if !strings.Contains(doc, needle) { |
| | 345 | + t.Errorf("permissions.md does not mention action %q (looking for %s)", a, needle) |
| | 346 | + } |
| | 347 | + } |
| | 348 | +} |