package httpapi import ( "context" "net/http" "testing" "time" "cloudipvalidator/internal/openstack" ) // slowDetachOS delays every Disassociate so the client can give up first. type slowDetachOS struct { *openstack.MockClient delay time.Duration } func (s slowDetachOS) DisassociateFloatingIP(ctx context.Context, fipID string) error { time.Sleep(s.delay) return s.MockClient.DisassociateFloatingIP(ctx, fipID) } // A client that gives up (a dashboard or curl timeout) must not abort // "clear queue" halfway: on 2026-10-02 the request context was cancelled after // part of the floating IPs were detached, the database was left untouched and // the checks kept running. func TestClearQueueSurvivesClientDisconnect(t *testing.T) { fc, d, orch, mock := newConfigTestHarness(t) ctx := context.Background() mock.Seed("fip-1", "1.2.3.4", "svc") if err := d.RegisterValidator(ctx, "validator-1", "host", "port-1", "v"); err != nil { t.Fatal(err) } if err := d.SeedQueue(ctx, []string{"1.2.3.4", "5.6.7.8"}); err != nil { t.Fatal(err) } orch.Tick(ctx) // validator-1 takes 1.2.3.4 and attaches its floating IP if fip, _ := mock.GetFloatingIPByAddress(ctx, "1.2.3.4"); fip.PortID != "port-1" { t.Fatalf("setup: floating ip not attached (port %q)", fip.PortID) } orch.OS = slowDetachOS{MockClient: mock, delay: 400 * time.Millisecond} reqCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() // No body: the server only notices that the client has left (and cancels // the request context) when it is not waiting for a request body. req, _ := http.NewRequestWithContext(reqCtx, http.MethodPost, fc.base+"/api/v1/admin/ips/clear", nil) if resp, err := fc.client.Do(req); err == nil { resp.Body.Close() t.Fatalf("the client was expected to time out, got status %d", resp.StatusCode) } deadline := time.Now().Add(5 * time.Second) for { ips, _ := d.ListIPs(ctx) fip, _ := mock.GetFloatingIPByAddress(ctx, "1.2.3.4") if len(ips) == 0 && fip.PortID == "" { return } if time.Now().After(deadline) { t.Fatalf("clear did not finish after the client left: %d rows, floating ip on port %q", len(ips), fip.PortID) } time.Sleep(50 * time.Millisecond) } }