package console import ( "errors" "reflect" "testing" "time" "github.com/carlosframework/platform/internal/svcrole" ) func TestCanonicalRungs(t *testing.T) { for _, tc := range []struct { in []string want string }{ {nil, ""}, {[]string{" ", ""}, ""}, {[]string{"edge"}, "edge"}, {[]string{"edge", " canary ", "edge", ""}, "canary,edge"}, {[]string{"beta", "canary", "edge"}, "canary,edge,beta"}, } { got, err := canonicalRungs(tc.in) if err != nil || got != tc.want { t.Errorf("canonicalRungs(%q) = %q, %v; want %q", tc.in, got, err, tc.want) } } if _, err := canonicalRungs([]string{"edge", "stable"}); !errors.Is(err, ErrStableNotDelegable) { t.Errorf("stable: err = %v, want ErrStableNotDelegable", err) } for _, bad := range []string{"Edge", "canary/x", "prod", "edge;beta"} { var ue *UnknownRungError if _, err := canonicalRungs([]string{bad}); !errors.As(err, &ue) || ue.Rung != bad { t.Errorf("canonicalRungs(%q) err = %v, want UnknownRungError naming it", bad, err) } } } func TestParseStoredRungs(t *testing.T) { if got, ok := parseStoredRungs("canary,edge"); !ok || !reflect.DeepEqual(got, []string{"canary", "edge"}) { t.Fatalf("parseStoredRungs(canary,edge) = %v, %v", got, ok) } // Not canonical, empty, or naming stable: never a usable scope. for _, bad := range []string{"", "edge,canary", "edge,edge", "edge,stable", "edge,", " edge"} { if got, ok := parseStoredRungs(bad); ok { t.Errorf("parseStoredRungs(%q) = %v, true; want false", bad, got) } } } func TestMintDeployRules(t *testing.T) { st := newDeployStore(t) now := time.Now() mint := func(app string, role svcrole.Role, rungs []string) error { _, err := st.NewServicePrincipalScoped(1, "ci", app, role, rungs, nil, "paul@example.test", now) return err } if err := mint("", svcrole.Deploy, []string{"edge"}); !errors.Is(err, ErrAccountScopedDeploy) { t.Errorf("deploy with no app: %v", err) } if err := mint("hello", svcrole.Deploy, nil); !errors.Is(err, ErrDeployNeedsRungs) { t.Errorf("deploy with no rungs: %v", err) } if err := mint("hello", svcrole.Deploy, []string{"stable"}); !errors.Is(err, ErrStableNotDelegable) { t.Errorf("deploy with stable: %v", err) } if err := mint("hello", svcrole.Publish, []string{"edge"}); !errors.Is(err, ErrRungsNeedDeploy) { t.Errorf("publish with rungs: %v", err) } // The old entry point cannot mint a deploy credential: it has no rungs. if _, err := st.NewServicePrincipal(1, "ci", "hello", svcrole.Deploy, "paul@example.test", now); !errors.Is(err, ErrDeployNeedsRungs) { t.Errorf("NewServicePrincipal(deploy): %v", err) } } func TestDeployPrincipalReadsBackItsRungs(t *testing.T) { st := newDeployStore(t) now := time.Now() raw, err := st.NewServicePrincipalScoped(1, "ci", "hello", svcrole.Deploy, []string{"edge", "canary"}, nil, "paul@example.test", now) if err != nil { t.Fatal(err) } p, ok := st.CLIPrincipal(raw, now) if !ok || p.Role != svcrole.Deploy || p.App != "hello" || !reflect.DeepEqual(p.Rungs, []string{"canary", "edge"}) { t.Fatalf("CLIPrincipal = %+v, %v", p, ok) } // Review Focus 3: rotation keeps the rungs, and the old secret dies. fresh, err := st.RotateServicePrincipal(1, "ci", now) if err != nil { t.Fatal(err) } if _, ok := st.CLIPrincipal(raw, now); ok { t.Fatal("the old secret still resolves after rotate") } p, ok = st.CLIPrincipal(fresh, now) if !ok || !reflect.DeepEqual(p.Rungs, []string{"canary", "edge"}) { t.Fatalf("after rotate: %+v, %v", p, ok) } rows, err := st.ServicePrincipals(1) if err != nil || len(rows) != 1 || rows[0].Rungs != "canary,edge" || rows[0].RungList() != "canary, edge" { t.Fatalf("ServicePrincipals = %+v, %v", rows, err) } } func TestDeployRowWithBadRungsDoesNotResolve(t *testing.T) { st := newDeployStore(t) now := time.Now() for i, rungs := range []string{"", "edge,canary", "stable", "edge,beta,"} { raw, err := st.NewServicePrincipalScoped(1, "ci"+string(rune('a'+i)), "hello", svcrole.Deploy, []string{"edge"}, nil, "paul@example.test", now) if err != nil { t.Fatal(err) } if _, err := st.db.Exec(`UPDATE cli_tokens SET rungs = ? WHERE token_hash = ?`, rungs, hashToken(raw)); err != nil { t.Fatal(err) } if p, ok := st.CLIPrincipal(raw, now); ok { t.Errorf("rungs %q: resolved to %+v; want refused", rungs, p) } } // A deploy row whose app was emptied (rungs still valid) is refused too: // a deploy credential is never account-wide. raw, err := st.NewServicePrincipalScoped(1, "noapp", "hello", svcrole.Deploy, []string{"edge"}, nil, "paul@example.test", now) if err != nil { t.Fatal(err) } if _, err := st.db.Exec(`UPDATE cli_tokens SET app = '' WHERE token_hash = ?`, hashToken(raw)); err != nil { t.Fatal(err) } if p, ok := st.CLIPrincipal(raw, now); ok { t.Errorf("a deploy row with app '' resolved to %+v; want refused", p) } // A non-deploy row that somehow carries rungs is refused too. raw, err = st.NewServicePrincipalScoped(1, "pub", "hello", svcrole.Publish, nil, nil, "paul@example.test", now) if err != nil { t.Fatal(err) } if _, err := st.db.Exec(`UPDATE cli_tokens SET rungs = 'edge' WHERE token_hash = ?`, hashToken(raw)); err != nil { t.Fatal(err) } if _, ok := st.CLIPrincipal(raw, now); ok { t.Error("a publish row carrying rungs resolved") } } // newDeployStore is a bare console store, enough for the store-level tests. func newDeployStore(t *testing.T) *Store { t.Helper() st, err := Open(t.TempDir() + "/console.db") if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) return st }