package server import ( "encoding/json" "fmt" "testing" "git.wntrmute.dev/kyle/mcr/internal/db" ) func TestAdminPolicyCRUDCycle(t *testing.T) { database := openAdminTestDB(t) router, reloader := buildAdminRouter(t, database) // Create a rule. createBody := `{ "priority": 50, "description": "allow CI push", "effect": "allow", "roles": ["ci"], "actions": ["registry:push", "registry:pull"], "repositories": ["production/*"], "enabled": true }` rr := adminReq(t, router, "POST", "/v1/policy/rules/", createBody) if rr.Code != 201 { t.Fatalf("create status: got %d, want 201; body: %s", rr.Code, rr.Body.String()) } if reloader.reloadCount != 1 { t.Fatalf("reload count after create: got %d, want 1", reloader.reloadCount) } var created db.PolicyRuleRow if err := json.NewDecoder(rr.Body).Decode(&created); err != nil { t.Fatalf("decode created: %v", err) } if created.ID == 0 { t.Fatal("expected non-zero ID") } if created.Effect != "allow" { t.Fatalf("effect: got %q, want %q", created.Effect, "allow") } if len(created.Roles) != 1 || created.Roles[0] != "ci" { t.Fatalf("roles: got %v, want [ci]", created.Roles) } if created.CreatedBy != "admin-uuid" { t.Fatalf("created_by: got %q, want %q", created.CreatedBy, "admin-uuid") } // Get the rule. rr = adminReq(t, router, "GET", fmt.Sprintf("/v1/policy/rules/%d", created.ID), "") if rr.Code != 200 { t.Fatalf("get status: got %d, want 200", rr.Code) } var got db.PolicyRuleRow if err := json.NewDecoder(rr.Body).Decode(&got); err != nil { t.Fatalf("decode got: %v", err) } if got.ID != created.ID { t.Fatalf("id: got %d, want %d", got.ID, created.ID) } // List rules. rr = adminReq(t, router, "GET", "/v1/policy/rules/", "") if rr.Code != 200 { t.Fatalf("list status: got %d, want 200", rr.Code) } var rules []db.PolicyRuleRow if err := json.NewDecoder(rr.Body).Decode(&rules); err != nil { t.Fatalf("decode list: %v", err) } if len(rules) != 1 { t.Fatalf("rule count: got %d, want 1", len(rules)) } // Update the rule. updateBody := `{"priority": 25, "description": "updated CI push"}` rr = adminReq(t, router, "PATCH", fmt.Sprintf("/v1/policy/rules/%d", created.ID), updateBody) if rr.Code != 200 { t.Fatalf("update status: got %d, want 200; body: %s", rr.Code, rr.Body.String()) } if reloader.reloadCount != 2 { t.Fatalf("reload count after update: got %d, want 2", reloader.reloadCount) } var updated db.PolicyRuleRow if err := json.NewDecoder(rr.Body).Decode(&updated); err != nil { t.Fatalf("decode updated: %v", err) } if updated.Priority != 25 { t.Fatalf("updated priority: got %d, want 25", updated.Priority) } if updated.Description != "updated CI push" { t.Fatalf("updated description: got %q, want %q", updated.Description, "updated CI push") } // Effect should be unchanged. if updated.Effect != "allow" { t.Fatalf("updated effect: got %q, want %q (unchanged)", updated.Effect, "allow") } // Delete the rule. rr = adminReq(t, router, "DELETE", fmt.Sprintf("/v1/policy/rules/%d", created.ID), "") if rr.Code != 204 { t.Fatalf("delete status: got %d, want 204", rr.Code) } if reloader.reloadCount != 3 { t.Fatalf("reload count after delete: got %d, want 3", reloader.reloadCount) } // Verify it's gone. rr = adminReq(t, router, "GET", fmt.Sprintf("/v1/policy/rules/%d", created.ID), "") if rr.Code != 404 { t.Fatalf("after delete status: got %d, want 404", rr.Code) } } func TestAdminCreatePolicyRuleValidation(t *testing.T) { database := openAdminTestDB(t) router, _ := buildAdminRouter(t, database) tests := []struct { name string body string want int }{ { name: "priority too low", body: `{"priority":0,"description":"test","effect":"allow","actions":["registry:pull"]}`, want: 400, }, { name: "missing description", body: `{"priority":1,"effect":"allow","actions":["registry:pull"]}`, want: 400, }, { name: "invalid effect", body: `{"priority":1,"description":"test","effect":"maybe","actions":["registry:pull"]}`, want: 400, }, { name: "no actions", body: `{"priority":1,"description":"test","effect":"allow","actions":[]}`, want: 400, }, { name: "invalid action", body: `{"priority":1,"description":"test","effect":"allow","actions":["bogus:action"]}`, want: 400, }, { name: "bad JSON", body: `not json`, want: 400, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { rr := adminReq(t, router, "POST", "/v1/policy/rules/", tc.body) if rr.Code != tc.want { t.Fatalf("status: got %d, want %d; body: %s", rr.Code, tc.want, rr.Body.String()) } }) } } func TestAdminUpdatePolicyRuleValidation(t *testing.T) { database := openAdminTestDB(t) router, _ := buildAdminRouter(t, database) // Create a rule to update. createBody := `{"priority":50,"description":"test","effect":"allow","actions":["registry:pull"]}` rr := adminReq(t, router, "POST", "/v1/policy/rules/", createBody) if rr.Code != 201 { t.Fatalf("create status: got %d, want 201", rr.Code) } var created db.PolicyRuleRow if err := json.NewDecoder(rr.Body).Decode(&created); err != nil { t.Fatalf("decode: %v", err) } tests := []struct { name string body string want int }{ { name: "priority too low", body: `{"priority":0}`, want: 400, }, { name: "invalid effect", body: `{"effect":"maybe"}`, want: 400, }, { name: "empty actions", body: `{"actions":[]}`, want: 400, }, { name: "invalid action", body: `{"actions":["bogus:action"]}`, want: 400, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { rr := adminReq(t, router, "PATCH", fmt.Sprintf("/v1/policy/rules/%d", created.ID), tc.body) if rr.Code != tc.want { t.Fatalf("status: got %d, want %d; body: %s", rr.Code, tc.want, rr.Body.String()) } }) } } func TestAdminUpdatePolicyRuleNotFound(t *testing.T) { database := openAdminTestDB(t) router, _ := buildAdminRouter(t, database) rr := adminReq(t, router, "PATCH", "/v1/policy/rules/9999", `{"description":"nope"}`) if rr.Code != 404 { t.Fatalf("status: got %d, want 404", rr.Code) } } func TestAdminDeletePolicyRuleNotFound(t *testing.T) { database := openAdminTestDB(t) router, _ := buildAdminRouter(t, database) rr := adminReq(t, router, "DELETE", "/v1/policy/rules/9999", "") if rr.Code != 404 { t.Fatalf("status: got %d, want 404", rr.Code) } } func TestAdminGetPolicyRuleNotFound(t *testing.T) { database := openAdminTestDB(t) router, _ := buildAdminRouter(t, database) rr := adminReq(t, router, "GET", "/v1/policy/rules/9999", "") if rr.Code != 404 { t.Fatalf("status: got %d, want 404", rr.Code) } } func TestAdminGetPolicyRuleInvalidID(t *testing.T) { database := openAdminTestDB(t) router, _ := buildAdminRouter(t, database) rr := adminReq(t, router, "GET", "/v1/policy/rules/not-a-number", "") if rr.Code != 400 { t.Fatalf("status: got %d, want 400", rr.Code) } } func TestAdminPolicyRulesNonAdmin(t *testing.T) { database := openAdminTestDB(t) router := buildNonAdminRouter(t, database) // All policy rule endpoints require admin. endpoints := []struct { method string path string body string }{ {"GET", "/v1/policy/rules/", ""}, {"POST", "/v1/policy/rules/", `{"priority":1,"description":"test","effect":"allow","actions":["registry:pull"]}`}, {"GET", "/v1/policy/rules/1", ""}, {"PATCH", "/v1/policy/rules/1", `{"description":"updated"}`}, {"DELETE", "/v1/policy/rules/1", ""}, } for _, ep := range endpoints { t.Run(ep.method+" "+ep.path, func(t *testing.T) { rr := adminReq(t, router, ep.method, ep.path, ep.body) if rr.Code != 403 { t.Fatalf("status: got %d, want 403; body: %s", rr.Code, rr.Body.String()) } }) } } func TestAdminUpdatePolicyRuleEnabled(t *testing.T) { database := openAdminTestDB(t) router, _ := buildAdminRouter(t, database) // Create an enabled rule. createBody := `{"priority":50,"description":"test","effect":"allow","actions":["registry:pull"],"enabled":true}` rr := adminReq(t, router, "POST", "/v1/policy/rules/", createBody) if rr.Code != 201 { t.Fatalf("create status: got %d, want 201", rr.Code) } var created db.PolicyRuleRow if err := json.NewDecoder(rr.Body).Decode(&created); err != nil { t.Fatalf("decode: %v", err) } if !created.Enabled { t.Fatal("expected created rule to be enabled") } // Disable it. rr = adminReq(t, router, "PATCH", fmt.Sprintf("/v1/policy/rules/%d", created.ID), `{"enabled":false}`) if rr.Code != 200 { t.Fatalf("update status: got %d, want 200; body: %s", rr.Code, rr.Body.String()) } var updated db.PolicyRuleRow if err := json.NewDecoder(rr.Body).Decode(&updated); err != nil { t.Fatalf("decode: %v", err) } if updated.Enabled { t.Fatal("expected updated rule to be disabled") } } func TestAdminListPolicyRulesEmpty(t *testing.T) { database := openAdminTestDB(t) router, _ := buildAdminRouter(t, database) rr := adminReq(t, router, "GET", "/v1/policy/rules/", "") if rr.Code != 200 { t.Fatalf("status: got %d, want 200", rr.Code) } var rules []db.PolicyRuleRow if err := json.NewDecoder(rr.Body).Decode(&rules); err != nil { t.Fatalf("decode: %v", err) } if len(rules) != 0 { t.Fatalf("rule count: got %d, want 0", len(rules)) } }