package gcp import ( "context" "errors" "net/http" "net/http/httptest" "net/url" "sync" "syscall" "testing" compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/googleapi" "google.golang.org/api/option" "google.golang.org/protobuf/proto" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider" ) func TestCheck_reachable(t *testing.T) { t.Parallel() api := &fakeAPI{} p := newTestProvider(api) got := p.Check(context.Background()) if len(got) != 1 { t.Fatalf("Check() returned %d targets, want 1", len(got)) } ts := got[0] if !ts.Ready || ts.Reason != provider.ReasonReachable || ts.Message != "" { t.Errorf("Check() = %+v, want ready and Reachable", ts) } if ts.Name != "my-project" || ts.Address != "compute.googleapis.com" { t.Errorf("target = %q at %q, want my-project at compute.googleapis.com", ts.Name, ts.Address) } want := &computepb.AggregatedListInstancesRequest{ Project: "my-project", Filter: proto.String("labels.proxy-operator-managed = true"), MaxResults: proto.Uint32(1), ReturnPartialSuccess: proto.Bool(true), } if !proto.Equal(api.probeReq, want) { t.Errorf("probe request = %v, want %v", api.probeReq, want) } } func TestCheck_failureReasons(t *testing.T) { t.Parallel() ended, cancel := context.WithCancel(context.Background()) cancel() tests := []struct { name string ctx context.Context err error want string }{ {"unauthorized", context.Background(), gerr(401), provider.ReasonUnauthorized}, {"forbidden", context.Background(), gerr(403, "forbidden"), provider.ReasonForbidden}, {"api disabled", context.Background(), gerr(403, "accessNotConfigured"), provider.ReasonForbidden}, {"server error", context.Background(), gerr(500), provider.ReasonAPIError}, {"deadline", context.Background(), &url.Error{ Op: "Get", URL: "https://compute.googleapis.com/", Err: context.DeadlineExceeded, }, provider.ReasonTimeout}, {"context ended", ended, context.Canceled, provider.ReasonTimeout}, {"connection refused", context.Background(), &url.Error{ Op: "Get", URL: "https://compute.googleapis.com/", Err: syscall.ECONNREFUSED, }, provider.ReasonUnreachable}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() p := newTestProvider(&fakeAPI{probeErr: tc.err}) got := p.Check(tc.ctx) if len(got) != 1 { t.Fatalf("Check() returned %d targets, want 1", len(got)) } if got[0].Ready || got[0].Reason != tc.want || got[0].Message != tc.err.Error() { t.Errorf("Check() = %+v, want not ready, reason %s, message %q", got[0], tc.want, tc.err.Error()) } }) } } // TestRealInstances_Probe runs the real SDK client against a local server, // since the point of Probe is how many requests the SDK makes. func TestRealInstances_Probe(t *testing.T) { t.Parallel() tests := []struct { name string status int body string wantCode int }{ { name: "fetches only the first page", status: http.StatusOK, body: `{"items":{"zones/europe-west1-b":{"instances":[{"name":"proxy-a"}]}},"nextPageToken":"page-2"}`, }, { name: "empty project", status: http.StatusOK, body: `{}`, }, { name: "api error keeps its status", status: http.StatusForbidden, body: `{"error":{"code":403,"message":"Compute Engine API has not been used","errors":[{"reason":"accessNotConfigured"}]}}`, wantCode: http.StatusForbidden, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() var ( mu sync.Mutex reqs []*url.URL ) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() reqs = append(reqs, r.URL) mu.Unlock() w.Header().Set("Content-Type", "application/json") w.WriteHeader(tc.status) _, _ = w.Write([]byte(tc.body)) })) t.Cleanup(srv.Close) ctx := context.Background() c, err := compute.NewInstancesRESTClient(ctx, option.WithEndpoint(srv.URL), option.WithoutAuthentication()) if err != nil { t.Fatalf("NewInstancesRESTClient: %v", err) } t.Cleanup(func() { _ = c.Close() }) err = (&realInstances{client: c}).Probe(ctx, &computepb.AggregatedListInstancesRequest{ Project: "my-project", MaxResults: proto.Uint32(1), }) var gerr *googleapi.Error switch { case tc.wantCode == 0 && err != nil: t.Fatalf("Probe() = %v, want nil", err) case tc.wantCode != 0 && (!errors.As(err, &gerr) || gerr.Code != tc.wantCode): t.Fatalf("Probe() = %v, want a googleapi error with code %d", err, tc.wantCode) } mu.Lock() defer mu.Unlock() if len(reqs) != 1 { t.Fatalf("server saw %d requests, want 1", len(reqs)) } if got := reqs[0].Query().Get("maxResults"); got != "1" { t.Errorf("maxResults = %q, want 1", got) } if want := "/compute/v1/projects/my-project/aggregated/instances"; reqs[0].Path != want { t.Errorf("path = %q, want %q", reqs[0].Path, want) } }) } }