package kvm2 import ( "context" "errors" "fmt" "strings" "testing" "time" "github.com/digitalocean/go-libvirt" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider" ) func TestGet_afterDeleteReturnsNotFound(t *testing.T) { t.Parallel() fx := newFixture(t, 1) id := fx.create(testName) if err := fx.p.Delete(fx.ctx, id); err != nil { t.Fatalf("Delete() error = %v", err) } inst, err := fx.p.Get(fx.ctx, id) if inst != nil { t.Errorf("Get after Delete = %+v, want nil", inst) } assertProviderError(t, err, "get", provider.ErrNotFound) } func TestGet_duringDeleteStillReturnsInstance(t *testing.T) { t.Parallel() t.Run("destroyed but still defined is stopped", func(t *testing.T) { t.Parallel() fx := newFixture(t, 1) id := fx.create(testName) fx.withDomain(testName, func(d *fakeDomain) { d.state = libvirt.DomainShutoff }) inst := fx.get(id) if inst.State != provider.StateStopped || inst.UID != testUID { t.Errorf("Get = %+v, want Stopped with uid %s", inst, testUID) } }) t.Run("undefine in flight still answers", func(t *testing.T) { t.Parallel() fx := newFixture(t, 1) id := fx.create(testName) release := fx.fake.blockOn("DomainUndefineFlags") done := make(chan error, 1) go func() { done <- fx.p.Delete(fx.ctx, id) }() waitFor(t, "the undefine to block", func() bool { return fx.fake.callCount("DomainUndefineFlags") == 1 }) inst, err := fx.p.Get(fx.ctx, id) if err != nil { t.Fatalf("Get during Delete error = %v, want the instance", err) } if inst.State != provider.StateStopped || inst.ID != id { t.Errorf("Get during Delete = %+v, want Stopped %s", inst, id) } close(release) if err := <-done; err != nil { t.Fatalf("Delete() error = %v", err) } _, err = fx.p.Get(fx.ctx, id) assertProviderError(t, err, "get", provider.ErrNotFound) }) } func TestGet_backendBusyIsTransientNeverNotFound(t *testing.T) { t.Parallel() tests := []struct { name string err error }{ {name: "operation invalid", err: fakeErr(libvirt.ErrOperationInvalid, "Requested operation is not valid: domain is being migrated")}, {name: "call timed out", err: fmt.Errorf("%w: libvirt call DomainGetState on beelink exceeded its 30s budget: %w", provider.ErrTransient, context.DeadlineExceeded)}, {name: "unknown libvirt code", err: fakeErr(libvirt.ErrInternalError, "internal error: qemu monitor unresponsive")}, {name: "not a libvirt error", err: errors.New("read tcp: connection reset by peer")}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() fx := newFixture(t, 1) id := fx.create(testName) fx.fake.failWith("DomainGetState", tc.err) inst, err := fx.p.Get(fx.ctx, id) if inst != nil { t.Errorf("Get = %+v, want nil", inst) } assertProviderError(t, err, "get", provider.ErrTransient) if errors.Is(err, provider.ErrNotFound) { t.Errorf("Get error = %v, must never be NotFound", err) } }) } } func TestGet_runningWithoutIPIsProvisioning(t *testing.T) { t.Parallel() fx := newFixture(t, 1) inst := fx.get(fx.create(testName)) if inst.State != provider.StateProvisioning || inst.IP != "" { t.Errorf("Get = %+v, want Provisioning with no IP", inst) } } func TestGet_runningWithIPIsRunning(t *testing.T) { t.Parallel() fx := newFixture(t, 1) id := fx.create(testName) fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcAgent, "127.0.0.1", "169.254.1.2", "10.0.0.5") inst := fx.get(id) if inst.State != provider.StateRunning || inst.IP != "10.0.0.5" { t.Errorf("Get = %+v, want Running with 10.0.0.5", inst) } if got := fx.logs.matching("kvm2 domain fetched"); len(got) != 1 || !strings.Contains(got[0], `"ip"="10.0.0.5"`) || !strings.Contains(got[0], `"state"="Running"`) { t.Errorf("fetched line = %v, want state and ip", got) } } func TestGet_agentPreferredOverARP(t *testing.T) { t.Parallel() fx := newFixture(t, 1) id := fx.create(testName) fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcAgent, "10.0.0.5") fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcArp, "10.0.0.6") before := fx.fake.callCount("DomainInterfaceAddresses") inst := fx.get(id) if inst.IP != "10.0.0.5" { t.Errorf("IP = %q, want the agent's 10.0.0.5", inst.IP) } if got := fx.fake.callCount("DomainInterfaceAddresses") - before; got != 1 { t.Errorf("DomainInterfaceAddresses calls = %d, want 1 (ARP not consulted)", got) } } func TestGet_arpFallbackWhenAgentFails(t *testing.T) { t.Parallel() fx := newFixture(t, 2) id := fx.create(testName) fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcArp, "10.0.0.6") var calls int fx.fake.mu.Lock() fx.fake.hooks["DomainInterfaceAddresses"] = func() error { calls++ if calls%2 == 1 { return fakeErr(libvirt.ErrAgentUnresponsive, "Guest agent is not responding") } return nil } fx.fake.mu.Unlock() inst := fx.get(id) if inst.State != provider.StateRunning || inst.IP != "10.0.0.6" { t.Errorf("Get = %+v, want Running with ARP's 10.0.0.6", inst) } if got := fx.logs.matching("no addresses from source"); len(got) != 1 || !strings.Contains(got[0], `"source"="agent"`) { t.Errorf("agent failure line = %v", got) } } func TestGet_addressErrorIsProvisioning(t *testing.T) { t.Parallel() fx := newFixture(t, 1) id := fx.create(testName) fx.fake.failWith("DomainInterfaceAddresses", fakeErr(libvirt.ErrAgentUnresponsive, "Guest agent is not responding")) inst := fx.get(id) if inst.State != provider.StateProvisioning || inst.IP != "" { t.Errorf("Get = %+v, want Provisioning with no IP", inst) } } func TestGet_readsUIDAndCreatedAtFromBackend(t *testing.T) { t.Parallel() fx := newFixture(t, 1) created := time.Date(2026, 9, 16, 12, 0, 0, 0, time.UTC) fx.p.now = func() time.Time { return created } id := fx.create(testName) inst := fx.get(id) if inst.UID != testUID || !inst.CreatedAt.Equal(created) { t.Errorf("Get = %+v, want uid %s created %s", inst, testUID, created) } later := created.Add(48 * time.Hour) fx.withDomain(testName, func(d *fakeDomain) { d.metadata = managedMetadata("rewritten-uid", later) }) inst = fx.get(id) if inst.UID != "rewritten-uid" || !inst.CreatedAt.Equal(later) { t.Errorf("Get after metadata change = %+v, want uid rewritten-uid created %s (read back, not remembered)", inst, later) } } func TestGet_unmanagedDomainHasEmptyUID(t *testing.T) { t.Parallel() fx := newFixture(t, 1) fx.fake.addDomain("legacy-vm", libvirt.DomainRunning, "") inst := fx.get(formatProviderID(testHost, "legacy-vm")) if inst.UID != "" || !inst.CreatedAt.IsZero() || inst.State != provider.StateProvisioning { t.Errorf("Get = %+v, want empty uid, zero createdAt, Provisioning", inst) } } func TestGet_malformedIDIsPermanent(t *testing.T) { t.Parallel() ids := []string{"", "proxy-x", "beelink/proxy-x", "hosts/beelink/proxy-x", "hosts//domains/proxy-x", "hosts/beelink/domains/", "hosts/beelink/domains/a/b", "nodes/beelink/domains/proxy-x"} for _, id := range ids { t.Run(id, func(t *testing.T) { t.Parallel() fx := newFixture(t, 1) inst, err := fx.p.Get(fx.ctx, id) if inst != nil { t.Errorf("Get(%q) = %+v, want nil", id, inst) } assertProviderError(t, err, "get", provider.ErrPermanent) var pe *provider.Error if errors.As(err, &pe) && pe.ID != id { t.Errorf("error.ID = %q, want %q", pe.ID, id) } if got := fx.dialer.dials.Load(); got != 0 { t.Errorf("dials = %d, want 0", got) } }) } } func TestGet_unconfiguredHostIsPermanent(t *testing.T) { t.Parallel() fx := newFixture(t, 1) _, err := fx.p.Get(fx.ctx, formatProviderID("nas9", testName)) assertProviderError(t, err, "get", provider.ErrPermanent) if !strings.Contains(err.Error(), `"nas9"`) { t.Errorf("error = %v, want the host named", err) } if got := fx.dialer.dials.Load(); got != 0 { t.Errorf("dials = %d, want 0", got) } } func TestGet_unknownStateIsStopped(t *testing.T) { t.Parallel() fx := newFixture(t, 1) fx.fake.addDomain(testName, libvirt.DomainState(42), managedMetadata(testUID, time.Now())) inst := fx.get(formatProviderID(testHost, testName)) if inst.State != provider.StateStopped { t.Errorf("State = %s, want Stopped for an unknown backend state", inst.State) } }