feat:runscope test
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
|
||||
)
|
||||
|
||||
func runFixture() *models.WorkflowRun {
|
||||
return &models.WorkflowRun{
|
||||
RunID: "r1",
|
||||
ServerRuns: []models.ServerRun{
|
||||
{ServerID: "stg-1", Hostname: "staging-web"},
|
||||
{ServerID: "prod-1", Hostname: "prod-db"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// A run document names every host it touched, hostname included. A restricted
|
||||
// caller must see only its own, and must be told some entries are missing
|
||||
// without being told how many — the targets_restricted precedent.
|
||||
func TestScopeRunHidesOutOfScopeServerRuns(t *testing.T) {
|
||||
got := scopeRun(runFixture(), map[string]bool{"stg-1": true}, true)
|
||||
if len(got.ServerRuns) != 1 || got.ServerRuns[0].ServerID != "stg-1" {
|
||||
t.Fatalf("server_runs = %v, want only stg-1", got.ServerRuns)
|
||||
}
|
||||
for _, sr := range got.ServerRuns {
|
||||
if sr.Hostname == "prod-db" {
|
||||
t.Error("out-of-scope hostname survived filtering")
|
||||
}
|
||||
}
|
||||
if !got.ServersRestricted {
|
||||
t.Error("servers_restricted = false with an entry dropped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeRunLeavesUnrestrictedCallerWhole(t *testing.T) {
|
||||
got := scopeRun(runFixture(), nil, false)
|
||||
if len(got.ServerRuns) != 2 {
|
||||
t.Fatalf("server_runs = %v, want both", got.ServerRuns)
|
||||
}
|
||||
if got.ServersRestricted {
|
||||
t.Error("unrestricted caller told entries were restricted")
|
||||
}
|
||||
}
|
||||
|
||||
// A restricted caller whose scope happens to cover the whole run must not be
|
||||
// told anything was hidden — the flag is about disclosure, not about being
|
||||
// restricted in general.
|
||||
func TestScopeRunNoFlagWhenNothingDropped(t *testing.T) {
|
||||
got := scopeRun(runFixture(), map[string]bool{"stg-1": true, "prod-1": true}, true)
|
||||
if got.ServersRestricted {
|
||||
t.Error("servers_restricted set with nothing dropped")
|
||||
}
|
||||
}
|
||||
@@ -193,3 +193,36 @@ func TestCreateMonitorDecodesTargetFields(t *testing.T) {
|
||||
t.Errorf("Target.Keyword = %q", m.Target.Keyword)
|
||||
}
|
||||
}
|
||||
|
||||
// A runner is a server ID. buildMonitor must refuse one outright rather than
|
||||
// dropping it silently, or a model would believe it had pinned a check to an
|
||||
// agent it never reached.
|
||||
func TestBuildMonitorRefusesRunner(t *testing.T) {
|
||||
args := map[string]any{
|
||||
"name": "api health",
|
||||
"type": "http",
|
||||
"target": map[string]any{"url": "https://example.com"},
|
||||
"runner": "some-server-id",
|
||||
}
|
||||
if _, err := buildMonitor(args); err == nil {
|
||||
t.Fatal("buildMonitor accepted a runner argument")
|
||||
}
|
||||
}
|
||||
|
||||
// Without a runner it still builds, and never arms itself.
|
||||
func TestBuildMonitorWithoutRunnerIsDisabled(t *testing.T) {
|
||||
m, err := buildMonitor(map[string]any{
|
||||
"name": "api health",
|
||||
"type": "http",
|
||||
"target": map[string]any{"url": "https://example.com"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildMonitor: %v", err)
|
||||
}
|
||||
if m.Runner != "" {
|
||||
t.Errorf("Runner = %q, want empty so CreateMonitor defaults it to the control plane", m.Runner)
|
||||
}
|
||||
if m.Enabled {
|
||||
t.Error("monitor created enabled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,3 +111,77 @@ func TestScopedTargetsExcludeOutOfScopeServers(t *testing.T) {
|
||||
t.Errorf("scoped targets = %v, want only a", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A workflow run is dispatched to whatever ResolveTargetsScoped returns, so
|
||||
// the property Critical 1 restores is that a run of a production-targeting
|
||||
// workflow, started by a staging token, reaches nothing. Proved over
|
||||
// UnionTargets for the same reason as the test above: no database.
|
||||
func TestScopedRunOfOutOfScopeWorkflowReachesNothing(t *testing.T) {
|
||||
all := []models.Server{
|
||||
{ServerID: "prod-1", Tags: map[string]string{"env": "prod"}},
|
||||
{ServerID: "prod-2", Tags: map[string]string{"env": "prod"}},
|
||||
}
|
||||
|
||||
scope := map[string]string{"env": "staging"}
|
||||
visible := []models.Server{}
|
||||
for _, s := range all {
|
||||
if ServerInTokenScope(s, scope) {
|
||||
visible = append(visible, s)
|
||||
}
|
||||
}
|
||||
|
||||
// The workflow's own saved targets, both by ID and by tag.
|
||||
got := UnionTargets(visible, []string{"prod-1", "prod-2"}, map[string]string{"env": "prod"})
|
||||
if len(got) != 0 {
|
||||
t.Errorf("staging-scoped run resolved %v, want nothing", got)
|
||||
}
|
||||
// ResolveTargetsScoped turns that empty set into ErrNoTargets, which is
|
||||
// the same answer a workflow targeting no servers at all gives — so the
|
||||
// refusal does not tell the caller that production hosts exist.
|
||||
}
|
||||
|
||||
// The scheduler passes a nil scope because it acts as the system. That must
|
||||
// keep meaning "the whole fleet", never "nothing", or every scheduled workflow
|
||||
// would silently stop firing.
|
||||
func TestNilScopeIsUnrestrictedNotEmpty(t *testing.T) {
|
||||
all := []models.Server{
|
||||
{ServerID: "prod-1", Tags: map[string]string{"env": "prod"}},
|
||||
{ServerID: "stg-1", Tags: map[string]string{"env": "staging"}},
|
||||
}
|
||||
visible := []models.Server{}
|
||||
for _, s := range all {
|
||||
if ServerInTokenScope(s, nil) {
|
||||
visible = append(visible, s)
|
||||
}
|
||||
}
|
||||
if len(visible) != 2 {
|
||||
t.Errorf("nil scope admitted %d servers, want all %d", len(visible), len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// FilterVisibleServerIDs is what narrows a run document's ServerRuns and a
|
||||
// workflow's target list. The property that matters is that the caller is told
|
||||
// something was hidden without being told how much.
|
||||
func TestFilterVisibleServerIDsReportsHiddenWithoutCount(t *testing.T) {
|
||||
visible := map[string]bool{"a": true}
|
||||
|
||||
got, hidden := FilterVisibleServerIDs([]string{"a", "b", "c"}, visible, true)
|
||||
if len(got) != 1 || got[0] != "a" {
|
||||
t.Errorf("filtered = %v, want [a]", got)
|
||||
}
|
||||
if !hidden {
|
||||
t.Error("hidden = false with two ids dropped")
|
||||
}
|
||||
|
||||
// Dropping one and dropping ten are indistinguishable: hidden is a bool.
|
||||
_, hiddenOne := FilterVisibleServerIDs([]string{"a", "b"}, visible, true)
|
||||
if hiddenOne != hidden {
|
||||
t.Error("hidden distinguishes how many were dropped")
|
||||
}
|
||||
|
||||
// An unrestricted caller is never told anything was hidden.
|
||||
got, hidden = FilterVisibleServerIDs([]string{"a", "b"}, nil, false)
|
||||
if len(got) != 2 || hidden {
|
||||
t.Errorf("unrestricted filter = %v, %v; want everything and no hidden flag", got, hidden)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user