package db import ( "context" "errors" "testing" "time" "cloudipvalidator/internal/config" ) // claimAwaitingSelfCheck claims the next address for the validator and brings // it to awaiting_self_check, as the orchestrator does after the association. func claimAwaitingSelfCheck(t *testing.T, ctx context.Context, d *DB, validatorID string) *IPQueueItem { t.Helper() item, err := d.ClaimNextQueued(ctx, validatorID, time.Minute) if err != nil || item == nil { t.Fatalf("claim for %s: item=%+v err=%v", validatorID, item, err) } if err := d.SetFIPAssociated(ctx, item.ID, "fip-"+item.IPAddress, time.Minute); err != nil { t.Fatalf("set fip associated: %v", err) } return item } // A validator that failed the self-check of an address is not given that // address again, takes the next one instead, and another validator takes the // excluded one; the exclusion covers that address only. func TestClaimSkipsAddressExcludedForValidator(t *testing.T) { d, ctx := newTestDB(t) if err := d.SeedQueue(ctx, []string{"1.1.1.1", "2.2.2.2"}); err != nil { t.Fatal(err) } for _, v := range []string{"v1", "v2"} { if err := d.AdminCreateValidator(ctx, v, "port-"+v); err != nil { t.Fatal(err) } } first := claimAwaitingSelfCheck(t, ctx, d, "v1") if first.IPAddress != "1.1.1.1" { t.Fatalf("expected 1.1.1.1 first, got %s", first.IPAddress) } res, err := d.FailSelfCheck(ctx, first.ID, "v1", "ip echo timeout", 5) if err != nil { t.Fatalf("fail self-check: %v", err) } if res.Failed || res.NewRound || res.Failures != 1 { t.Fatalf("expected a plain retry after the first failure, got %+v", res) } back, _ := d.GetIP(ctx, first.ID) if back.State != IPQueued || back.RetryCount != 0 { t.Fatalf("expected queued with retry_count untouched, got state=%s retry_count=%d", back.State, back.RetryCount) } if v, _ := d.GetValidator(ctx, "v1"); v.State != ValidatorIdle { t.Fatalf("expected v1 freed to idle, got %s", v.State) } // v1 skips 1.1.1.1 (still ahead in the queue) and takes 2.2.2.2. second, err := d.ClaimNextQueued(ctx, "v1", time.Minute) if err != nil || second == nil || second.IPAddress != "2.2.2.2" { t.Fatalf("expected v1 to take 2.2.2.2, got %+v err=%v", second, err) } // v2 takes the excluded address. other, err := d.ClaimNextQueued(ctx, "v2", time.Minute) if err != nil || other == nil || other.IPAddress != "1.1.1.1" { t.Fatalf("expected v2 to take 1.1.1.1, got %+v err=%v", other, err) } } // When every working validator has failed the address in the round, a new // round starts and the exclusions lapse; an unreachable validator does not // count as working. func TestSelfCheckNewRoundLiftsExclusions(t *testing.T) { d, ctx := newTestDB(t) if err := d.SeedQueue(ctx, []string{"1.1.1.1"}); err != nil { t.Fatal(err) } for _, v := range []string{"v1", "v2"} { if err := d.AdminCreateValidator(ctx, v, "port-"+v); err != nil { t.Fatal(err) } } if _, err := d.Exec(`UPDATE validators SET state=? WHERE validator_id='v2'`, ValidatorUnreachable); err != nil { t.Fatal(err) } item := claimAwaitingSelfCheck(t, ctx, d, "v1") res, err := d.FailSelfCheck(ctx, item.ID, "v1", "x", 5) if err != nil { t.Fatal(err) } if !res.NewRound { t.Fatalf("expected a new round (the only working validator failed it), got %+v", res) } again, err := d.ClaimNextQueued(ctx, "v1", time.Minute) if err != nil || again == nil || again.IPAddress != "1.1.1.1" { t.Fatalf("expected v1 to get the address again in the new round, got %+v err=%v", again, err) } } // Reaching the ceiling gives the verdict fail; a re-submission starts a new // series (counter and exclusions) but keeps the failure history. func TestSelfCheckCeilingAndResubmit(t *testing.T) { d, ctx := newTestDB(t) if err := d.SeedQueue(ctx, []string{"1.1.1.1"}); err != nil { t.Fatal(err) } for _, v := range []string{"v1", "v2"} { if err := d.AdminCreateValidator(ctx, v, "port-"+v); err != nil { t.Fatal(err) } } item := claimAwaitingSelfCheck(t, ctx, d, "v1") if res, err := d.FailSelfCheck(ctx, item.ID, "v1", "x", 2); err != nil || res.Failed { t.Fatalf("below the ceiling: res=%+v err=%v", res, err) } item = claimAwaitingSelfCheck(t, ctx, d, "v2") res, err := d.FailSelfCheck(ctx, item.ID, "v2", "y", 2) if err != nil { t.Fatal(err) } if !res.Failed || res.Failures != 2 || len(res.Validators) != 2 || res.Validators[0] != "v1" || res.Validators[1] != "v2" { t.Fatalf("expected fail at the ceiling on v1, v2, got %+v", res) } ip, _ := d.GetIP(ctx, item.ID) if ip.State != IPFailed || ip.OverallResult != ResultFail { t.Fatalf("expected failed/fail, got %s/%s", ip.State, ip.OverallResult) } if _, err := d.SubmitIPs(ctx, []string{"1.1.1.1"}); err != nil { t.Fatal(err) } var failures int if err := d.QueryRow(`SELECT sc_failures FROM ip_queue WHERE id=?`, item.ID).Scan(&failures); err != nil || failures != 0 { t.Fatalf("expected the series reset to 0, got %d err=%v", failures, err) } if got, err := d.ListSelfCheckFailedOn(ctx, ip.RegistryID, 0); err != nil || len(got) != 2 || got[0] != "v1" || got[1] != "v2" { t.Fatalf("expected the history kept (v1, v2), got %v err=%v", got, err) } // v1 failed it before the re-submission, but is no longer excluded. if again, err := d.ClaimNextQueued(ctx, "v1", time.Minute); err != nil || again == nil { t.Fatalf("expected v1 to claim the re-submitted address, got %+v err=%v", again, err) } } // A late report for an address the validator does not hold is refused. func TestFailSelfCheckRequiresAwaitingSelfCheckOnValidator(t *testing.T) { d, ctx := newTestDB(t) if err := d.SeedQueue(ctx, []string{"1.1.1.1"}); err != nil { t.Fatal(err) } for _, v := range []string{"v1", "v2"} { if err := d.AdminCreateValidator(ctx, v, "port-"+v); err != nil { t.Fatal(err) } } item := claimAwaitingSelfCheck(t, ctx, d, "v1") if _, err := d.FailSelfCheck(ctx, item.ID, "v2", "late", 5); !errors.Is(err, ErrInvalidState) { t.Fatalf("expected ErrInvalidState for another validator, got %v", err) } cur, _ := d.GetIP(ctx, item.ID) if got, _ := d.ListSelfCheckFailedOn(ctx, cur.RegistryID, 0); len(got) != 0 { t.Fatalf("a refused report must leave no history, got %v", got) } } func TestSelfCheckMaxAttemptsSetting(t *testing.T) { d, ctx := newTestDB(t) if err := d.BootstrapFromConfig(ctx, &config.ControlAPI{}); err != nil { t.Fatal(err) } if s, err := d.GetSettings(ctx); err != nil || s.SelfCheckMaxAttempts != 5 { t.Fatalf("expected default 5, got %+v err=%v", s, err) } for _, bad := range []int{0, -1, 51} { if err := d.SetSelfCheckMaxAttempts(ctx, bad); !errors.Is(err, ErrValidation) { t.Fatalf("expected ErrValidation for %d, got %v", bad, err) } } for _, ok := range []int{1, 50} { if err := d.SetSelfCheckMaxAttempts(ctx, ok); err != nil { t.Fatalf("set %d: %v", ok, err) } if s, _ := d.GetSettings(ctx); s.SelfCheckMaxAttempts != ok { t.Fatalf("expected %d, got %d", ok, s.SelfCheckMaxAttempts) } } }