diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asked_again_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asked_again_test.go new file mode 100644 index 00000000..844aa47e --- /dev/null +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asked_again_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "errors" + "fmt" + "testing" + "time" +) + +// scriptedSecrets answers each Get from the next of its errors, then with a value, counting the asks. +type scriptedSecrets struct { + errs []error + asked int +} + +func (f *scriptedSecrets) Get(context.Context, string) (string, int, error) { + f.asked++ + if f.asked <= len(f.errs) { + return "", 0, f.errs[f.asked-1] + } + return "tok", 1, nil +} + +// Issue 928: a refusal is asked again after RefusedFor, and a not-yet on every pass, so a vault that was down or +// that holds the value later fills the file with no change of the plan. +func TestARefusedSecretIsAskedAgainAfterRefusedForAndFilledWhenTheVaultHasIt(t *testing.T) { + now := time.Date(2026, 10, 16, 16, 20, 0, 0, time.UTC) + fake := &scriptedSecrets{errs: []error{errors.New("the vault refused it: the assignment g14.claude-code could not be read")}} + c := &Client{Node: "g14", Secrets: fake, Now: func() time.Time { return now }} + s := &state{secrets: map[string]heldSecret{}} + n := Need{Kind: "secret", Name: "gitea-read"} + name := "provision/novox/gitea-read/g14/claude-code" + + if _, _, fail := c.vaultValue(context.Background(), s, n, name); fail == "" { + t.Fatal("a refusal did not fail the need") + } + now = now.Add(RefusedFor - time.Second) + if _, _, fail := c.vaultValue(context.Background(), s, n, name); fail == "" || fake.asked != 1 { + t.Fatalf("within RefusedFor the vault was asked again (%d asks) or the refusal was dropped", fake.asked) + } + now = now.Add(time.Second) + v, wait, fail := c.vaultValue(context.Background(), s, n, name) + if fake.asked != 2 || v != "tok" || wait != "" || fail != "" { + t.Fatalf("after RefusedFor: %d asks, value %q, waits %q, fails %q; want asked again and filled", fake.asked, v, wait, fail) + } +} + +func TestANotYetSecretIsAskedAgainOnEveryPass(t *testing.T) { + now := time.Date(2026, 10, 16, 16, 20, 0, 0, time.UTC) + notYet := fmt.Errorf("%w: the vault holds no secret named x yet", errNotYet) + fake := &scriptedSecrets{errs: []error{notYet, notYet}} + c := &Client{Node: "g14", Secrets: fake, Now: func() time.Time { return now }} + s := &state{secrets: map[string]heldSecret{}} + n := Need{Kind: "secret", Name: "gitea-read"} + for i := 0; i < 2; i++ { + if _, wait, fail := c.vaultValue(context.Background(), s, n, "x"); wait == "" || fail != "" { + t.Fatalf("pass %d: waits %q fails %q, want a wait", i, wait, fail) + } + } + if v, _, _ := c.vaultValue(context.Background(), s, n, "x"); v != "tok" || fake.asked != 3 { + t.Fatalf("value %q after %d asks, want filled on the third", v, fake.asked) + } +}