package gcp import ( "context" "errors" "testing" "time" "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/protobuf/proto" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider" ) // fakeAPI implements the instancesAPI seam. type fakeAPI struct { insertReq *computepb.InsertInstanceRequest insertErr error getReq *computepb.GetInstanceRequest getInst *computepb.Instance getErr error deleteReq *computepb.DeleteInstanceRequest deleteErr error listReq *computepb.AggregatedListInstancesRequest listInsts []*computepb.Instance listErr error } func (f *fakeAPI) Insert(_ context.Context, req *computepb.InsertInstanceRequest) (string, error) { f.insertReq = req return "op-insert", f.insertErr } func (f *fakeAPI) Get(_ context.Context, req *computepb.GetInstanceRequest) (*computepb.Instance, error) { f.getReq = req return f.getInst, f.getErr } func (f *fakeAPI) Delete(_ context.Context, req *computepb.DeleteInstanceRequest) (string, error) { f.deleteReq = req return "op-delete", f.deleteErr } func (f *fakeAPI) AggregatedList(_ context.Context, req *computepb.AggregatedListInstancesRequest) ([]*computepb.Instance, error) { f.listReq = req return f.listInsts, f.listErr } func newTestProvider(api *fakeAPI) *Provider { return newWithAPI(provider.ProviderConfig{ Name: "gcp-eu", Type: "gcp", GCP: &provider.GCPConfig{Project: "my-project"}, }, api) } func TestCreate_returnsZoneQualifiedID(t *testing.T) { t.Parallel() api := &fakeAPI{} p := newTestProvider(api) id, err := p.Create(context.Background(), testCreateRequest()) if err != nil { t.Fatalf("Create: %v", err) } if want := "zones/europe-west1-b/instances/proxy-abc123def456ghij"; id != want { t.Errorf("providerID = %s, want %s", id, want) } if api.insertReq.Project != "my-project" || api.insertReq.Zone != "europe-west1-b" { t.Errorf("insert sent to %s/%s, want my-project/europe-west1-b", api.insertReq.Project, api.insertReq.Zone) } } func TestCreate_alreadyExistsIsSuccess(t *testing.T) { t.Parallel() api := &fakeAPI{insertErr: gerr(409)} p := newTestProvider(api) id, err := p.Create(context.Background(), testCreateRequest()) if err != nil { t.Fatalf("Create after crash (409): %v — alreadyExists must be success", err) } if want := "zones/europe-west1-b/instances/proxy-abc123def456ghij"; id != want { t.Errorf("providerID = %s, want %s", id, want) } } func TestCreate_incompletePlacementIsPermanent(t *testing.T) { t.Parallel() api := &fakeAPI{} p := newTestProvider(api) req := testCreateRequest() req.Placement.MachineType = "" _, err := p.Create(context.Background(), req) if provider.Class(err) != provider.ErrPermanent { t.Errorf("Class = %v, want ErrPermanent for missing placement", provider.Class(err)) } if api.insertReq != nil { t.Error("Insert was called despite invalid placement") } } func TestCreate_quotaErrorClassified(t *testing.T) { t.Parallel() p := newTestProvider(&fakeAPI{insertErr: gerr(403, "quotaExceeded")}) _, err := p.Create(context.Background(), testCreateRequest()) if provider.Class(err) != provider.ErrQuotaExceeded { t.Errorf("Class = %v, want ErrQuotaExceeded", provider.Class(err)) } } func TestGet_stateMapping(t *testing.T) { t.Parallel() tests := []struct { name string status string natIP string wantState provider.InstanceState wantIP string }{ {name: "provisioning", status: "PROVISIONING", wantState: provider.StateProvisioning}, {name: "staging", status: "STAGING", wantState: provider.StateProvisioning}, {name: "repairing", status: "REPAIRING", wantState: provider.StateProvisioning}, {name: "running without NatIP stays provisioning", status: "RUNNING", wantState: provider.StateProvisioning}, {name: "running with NatIP", status: "RUNNING", natIP: "34.1.2.3", wantState: provider.StateRunning, wantIP: "34.1.2.3"}, {name: "stopped", status: "STOPPED", wantState: provider.StateStopped}, {name: "suspended", status: "SUSPENDED", wantState: provider.StateStopped}, {name: "terminated", status: "TERMINATED", wantState: provider.StateTerminated}, {name: "unknown status maps to stopped for recreation", status: "SOMETHING_NEW", wantState: provider.StateStopped}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() inst := &computepb.Instance{ Name: proto.String("proxy-abc"), Status: proto.String(tc.status), CreationTimestamp: proto.String("2026-08-09T10:00:00+02:00"), Labels: map[string]string{ provider.LabelUID: "uid-1", }, } if tc.natIP != "" { inst.NetworkInterfaces = []*computepb.NetworkInterface{{ AccessConfigs: []*computepb.AccessConfig{{NatIP: proto.String(tc.natIP)}}, }} } p := newTestProvider(&fakeAPI{getInst: inst}) got, err := p.Get(context.Background(), "zones/europe-west1-b/instances/proxy-abc") if err != nil { t.Fatalf("Get: %v", err) } if got.State != tc.wantState || got.IP != tc.wantIP { t.Errorf("state/ip = %s/%q, want %s/%q", got.State, got.IP, tc.wantState, tc.wantIP) } if got.UID != "uid-1" { t.Errorf("UID = %q, want uid-1 (from the GC label)", got.UID) } if got.ID != "zones/europe-west1-b/instances/proxy-abc" { t.Errorf("ID = %s, want the zone-qualified providerID", got.ID) } if got.CreatedAt.IsZero() { t.Error("CreatedAt not parsed from creationTimestamp") } }) } } func TestGet_notFound(t *testing.T) { t.Parallel() p := newTestProvider(&fakeAPI{getErr: gerr(404)}) _, err := p.Get(context.Background(), "zones/z/instances/gone") if !errors.Is(err, provider.ErrNotFound) { t.Errorf("err = %v, want ErrNotFound", err) } } func TestGet_malformedProviderID(t *testing.T) { t.Parallel() p := newTestProvider(&fakeAPI{}) for _, id := range []string{"", "proxy-abc", "zones//instances/x", "zones/z/instances/", "z/zone/i/name"} { if _, err := p.Get(context.Background(), id); provider.Class(err) != provider.ErrPermanent { t.Errorf("Get(%q): Class = %v, want ErrPermanent", id, provider.Class(err)) } } } func TestDelete_notFoundIsSuccess(t *testing.T) { t.Parallel() api := &fakeAPI{deleteErr: gerr(404)} p := newTestProvider(api) if err := p.Delete(context.Background(), "zones/z/instances/gone"); err != nil { t.Errorf("Delete of missing instance: %v, want nil", err) } } func TestDelete_sendsParsedZoneAndName(t *testing.T) { t.Parallel() api := &fakeAPI{} p := newTestProvider(api) if err := p.Delete(context.Background(), "zones/us-east1-c/instances/proxy-xyz"); err != nil { t.Fatalf("Delete: %v", err) } if api.deleteReq.Zone != "us-east1-c" || api.deleteReq.Instance != "proxy-xyz" { t.Errorf("delete sent %s/%s, want us-east1-c/proxy-xyz", api.deleteReq.Zone, api.deleteReq.Instance) } } func TestListByTag(t *testing.T) { t.Parallel() api := &fakeAPI{listInsts: []*computepb.Instance{{ Name: proto.String("proxy-old"), Status: proto.String("RUNNING"), Zone: proto.String("https://www.googleapis.com/compute/v1/projects/my-project/zones/europe-west1-b"), CreationTimestamp: proto.String(time.Now().Format(time.RFC3339)), Labels: map[string]string{ provider.LabelManaged: provider.LabelManagedYes, provider.LabelUID: "uid-orphan", }, NetworkInterfaces: []*computepb.NetworkInterface{{ AccessConfigs: []*computepb.AccessConfig{{NatIP: proto.String("34.9.9.9")}}, }}, }}} p := newTestProvider(api) got, err := p.ListByTag(context.Background()) if err != nil { t.Fatalf("ListByTag: %v", err) } if want := "labels.proxy-operator-managed = true"; api.listReq.GetFilter() != want { t.Errorf("filter = %q, want %q", api.listReq.GetFilter(), want) } if !api.listReq.GetReturnPartialSuccess() { t.Error("ReturnPartialSuccess not set — one unreachable zone would fail the whole GC sweep") } if len(got) != 1 { t.Fatalf("instances = %d, want 1", len(got)) } if got[0].ID != "zones/europe-west1-b/instances/proxy-old" { t.Errorf("ID = %s, want the zone parsed out of the URL-style zone field", got[0].ID) } if got[0].UID != "uid-orphan" || got[0].State != provider.StateRunning { t.Errorf("instance = %+v, want uid-orphan/Running", got[0]) } } func TestListByTag_errorPropagates(t *testing.T) { t.Parallel() p := newTestProvider(&fakeAPI{listErr: gerr(500)}) _, err := p.ListByTag(context.Background()) if provider.Class(err) != provider.ErrTransient { t.Errorf("Class = %v, want ErrTransient", provider.Class(err)) } } func TestParseProviderID_roundTrip(t *testing.T) { t.Parallel() id := formatProviderID("europe-west1-b", "proxy-abc") zone, name, err := parseProviderID(id) if err != nil || zone != "europe-west1-b" || name != "proxy-abc" { t.Errorf("round trip = %s/%s (%v), want europe-west1-b/proxy-abc", zone, name, err) } }