Files
mcias/internal/db/policy_test.go
Kyle Isom 052d3ed1b8 Add PG creds + policy/tags UI; fix lint and build
- internal/ui/ui.go: add PGCred, Tags to AccountDetailData; register
  PUT /accounts/{id}/pgcreds and PUT /accounts/{id}/tags routes; add
  pgcreds_form.html and tags_editor.html to shared template set; remove
  unused AccountTagsData; fix fieldalignment on PolicyRuleView, PoliciesData
- internal/ui/handlers_accounts.go: add handleSetPGCreds — encrypts
  password via crypto.SealAESGCM, writes audit EventPGCredUpdated, renders
  pgcreds_form fragment; password never echoed; load PG creds and tags in
  handleAccountDetail
- internal/ui/handlers_policy.go: fix handleSetAccountTags to render with
  AccountDetailData instead of removed AccountTagsData
- internal/ui/ui_test.go: add 5 PG credential UI tests
- web/templates/fragments/pgcreds_form.html: new fragment — metadata display
  + set/replace form; system accounts only; password write-only
- web/templates/fragments/tags_editor.html: new fragment — textarea editor
  with HTMX PUT for atomic tag replacement
- web/templates/fragments/policy_form.html: rewrite to use structured fields
  matching handleCreatePolicyRule (roles/account_types/actions multi-select,
  resource_type, subject_uuid, service_names, required_tags, checkbox)
- web/templates/policies.html: new policies management page
- web/templates/fragments/policy_row.html: new HTMX table row with toggle
  and delete
- web/templates/account_detail.html: add Tags card and PG Credentials card
- web/templates/base.html: add Policies nav link
- internal/server/server.go: remove ~220 lines of duplicate tag/policy
  handler code (real implementations are in handlers_policy.go)
- internal/policy/engine_wrapper.go: fix corrupted source; use errors.New
- internal/db/policy_test.go: use model.AccountTypeHuman constant
- cmd/mciasctl/main.go: add nolint:gosec to int(os.Stdin.Fd()) calls
- gofmt/goimports: db/policy_test.go, policy/defaults.go,
  policy/engine_test.go, ui/ui.go, cmd/mciasctl/main.go
- fieldalignment: model.PolicyRuleRecord, policy.Engine, policy.Rule,
  policy.RuleBody, ui.PolicyRuleView
Security: PG password encrypted AES-256-GCM with fresh random nonce before
storage; plaintext never logged or returned in any response; audit event
written on every credential write.
2026-03-11 23:24:03 -07:00

213 lines
5.8 KiB
Go

package db
import (
"errors"
"testing"
"git.wntrmute.dev/kyle/mcias/internal/model"
)
func TestCreateAndGetPolicyRule(t *testing.T) {
db := openTestDB(t)
ruleJSON := `{"actions":["pgcreds:read"],"resource_type":"pgcreds","effect":"allow"}`
rec, err := db.CreatePolicyRule("test rule", 50, ruleJSON, nil)
if err != nil {
t.Fatalf("CreatePolicyRule: %v", err)
}
if rec.ID == 0 {
t.Error("expected non-zero ID after create")
}
if rec.Priority != 50 {
t.Errorf("expected priority 50, got %d", rec.Priority)
}
if !rec.Enabled {
t.Error("new rule should be enabled by default")
}
got, err := db.GetPolicyRule(rec.ID)
if err != nil {
t.Fatalf("GetPolicyRule: %v", err)
}
if got.Description != "test rule" {
t.Errorf("expected description %q, got %q", "test rule", got.Description)
}
if got.RuleJSON != ruleJSON {
t.Errorf("rule_json mismatch: got %q", got.RuleJSON)
}
}
func TestGetPolicyRule_NotFound(t *testing.T) {
db := openTestDB(t)
_, err := db.GetPolicyRule(99999)
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got %v", err)
}
}
func TestListPolicyRules(t *testing.T) {
db := openTestDB(t)
_, _ = db.CreatePolicyRule("rule A", 100, `{"effect":"allow"}`, nil)
_, _ = db.CreatePolicyRule("rule B", 50, `{"effect":"deny"}`, nil)
_, _ = db.CreatePolicyRule("rule C", 200, `{"effect":"allow"}`, nil)
rules, err := db.ListPolicyRules(false)
if err != nil {
t.Fatalf("ListPolicyRules: %v", err)
}
if len(rules) != 3 {
t.Fatalf("expected 3 rules, got %d", len(rules))
}
// Should be ordered by priority ascending.
if rules[0].Priority > rules[1].Priority || rules[1].Priority > rules[2].Priority {
t.Errorf("rules not sorted by priority: %v %v %v",
rules[0].Priority, rules[1].Priority, rules[2].Priority)
}
}
func TestListPolicyRules_EnabledOnly(t *testing.T) {
db := openTestDB(t)
r1, _ := db.CreatePolicyRule("enabled rule", 100, `{"effect":"allow"}`, nil)
r2, _ := db.CreatePolicyRule("disabled rule", 100, `{"effect":"deny"}`, nil)
if err := db.SetPolicyRuleEnabled(r2.ID, false); err != nil {
t.Fatalf("SetPolicyRuleEnabled: %v", err)
}
all, err := db.ListPolicyRules(false)
if err != nil {
t.Fatalf("ListPolicyRules(all): %v", err)
}
if len(all) != 2 {
t.Errorf("expected 2 total rules, got %d", len(all))
}
enabled, err := db.ListPolicyRules(true)
if err != nil {
t.Fatalf("ListPolicyRules(enabledOnly): %v", err)
}
if len(enabled) != 1 {
t.Fatalf("expected 1 enabled rule, got %d", len(enabled))
}
if enabled[0].ID != r1.ID {
t.Errorf("wrong rule returned: got ID %d, want %d", enabled[0].ID, r1.ID)
}
}
func TestUpdatePolicyRule(t *testing.T) {
db := openTestDB(t)
rec, _ := db.CreatePolicyRule("original", 100, `{"effect":"allow"}`, nil)
newDesc := "updated description"
newPriority := 25
if err := db.UpdatePolicyRule(rec.ID, &newDesc, &newPriority, nil); err != nil {
t.Fatalf("UpdatePolicyRule: %v", err)
}
got, err := db.GetPolicyRule(rec.ID)
if err != nil {
t.Fatalf("GetPolicyRule after update: %v", err)
}
if got.Description != newDesc {
t.Errorf("expected description %q, got %q", newDesc, got.Description)
}
if got.Priority != newPriority {
t.Errorf("expected priority %d, got %d", newPriority, got.Priority)
}
// RuleJSON should be unchanged.
if got.RuleJSON != `{"effect":"allow"}` {
t.Errorf("rule_json should not change when not provided: %q", got.RuleJSON)
}
}
func TestUpdatePolicyRule_RuleJSON(t *testing.T) {
db := openTestDB(t)
rec, _ := db.CreatePolicyRule("rule", 100, `{"effect":"allow"}`, nil)
newJSON := `{"effect":"deny","roles":["auditor"]}`
if err := db.UpdatePolicyRule(rec.ID, nil, nil, &newJSON); err != nil {
t.Fatalf("UpdatePolicyRule (json only): %v", err)
}
got, err := db.GetPolicyRule(rec.ID)
if err != nil {
t.Fatalf("GetPolicyRule: %v", err)
}
if got.RuleJSON != newJSON {
t.Errorf("expected updated rule_json, got %q", got.RuleJSON)
}
// Description and priority unchanged.
if got.Description != "rule" {
t.Errorf("description should be unchanged, got %q", got.Description)
}
}
func TestSetPolicyRuleEnabled(t *testing.T) {
db := openTestDB(t)
rec, _ := db.CreatePolicyRule("toggle rule", 100, `{"effect":"allow"}`, nil)
if !rec.Enabled {
t.Fatal("new rule should be enabled")
}
if err := db.SetPolicyRuleEnabled(rec.ID, false); err != nil {
t.Fatalf("SetPolicyRuleEnabled(false): %v", err)
}
got, _ := db.GetPolicyRule(rec.ID)
if got.Enabled {
t.Error("rule should be disabled after SetPolicyRuleEnabled(false)")
}
if err := db.SetPolicyRuleEnabled(rec.ID, true); err != nil {
t.Fatalf("SetPolicyRuleEnabled(true): %v", err)
}
got, _ = db.GetPolicyRule(rec.ID)
if !got.Enabled {
t.Error("rule should be enabled after SetPolicyRuleEnabled(true)")
}
}
func TestDeletePolicyRule(t *testing.T) {
db := openTestDB(t)
rec, _ := db.CreatePolicyRule("to delete", 100, `{"effect":"allow"}`, nil)
if err := db.DeletePolicyRule(rec.ID); err != nil {
t.Fatalf("DeletePolicyRule: %v", err)
}
_, err := db.GetPolicyRule(rec.ID)
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound after delete, got %v", err)
}
}
func TestDeletePolicyRule_NonExistent(t *testing.T) {
db := openTestDB(t)
// Deleting a non-existent rule should be a no-op, not an error.
if err := db.DeletePolicyRule(99999); err != nil {
t.Errorf("DeletePolicyRule on nonexistent ID should not error: %v", err)
}
}
func TestCreatePolicyRule_WithCreatedBy(t *testing.T) {
db := openTestDB(t)
acct, _ := db.CreateAccount("policy-creator", model.AccountTypeHuman, "hash")
rec, err := db.CreatePolicyRule("by user", 100, `{"effect":"allow"}`, &acct.ID)
if err != nil {
t.Fatalf("CreatePolicyRule with createdBy: %v", err)
}
got, _ := db.GetPolicyRule(rec.ID)
if got.CreatedBy == nil || *got.CreatedBy != acct.ID {
t.Errorf("expected CreatedBy=%d, got %v", acct.ID, got.CreatedBy)
}
}