package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "gotest.tools/v3/assert" ) func TestGetDeviceCode(t *testing.T) { t.Parallel() t.Run("success", func(t *testing.T) { t.Parallel() var clientID, audience, scope, path string expectedState := State{ DeviceCode: "aDeviceCode", UserCode: "aUserCode", VerificationURI: "aVerificationURI", ExpiresIn: 60, } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.ParseForm() clientID = r.FormValue("client_id") audience = r.FormValue("audience") scope = r.FormValue("scope") path = r.URL.Path jsonState, err := json.Marshal(expectedState) assert.NilError(t, err) _, _ = w.Write(jsonState) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } state, err := api.GetDeviceCode(context.Background(), "anAudience") assert.NilError(t, err) assert.DeepEqual(t, expectedState, state) assert.Equal(t, clientID, "aClientID") assert.Equal(t, audience, "anAudience") assert.Equal(t, scope, "bork meow") assert.Equal(t, path, "/oauth/device/code") }) t.Run("error w/ description", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { jsonState, err := json.Marshal(TokenResponse{ ErrorDescription: "invalid audience", }) assert.NilError(t, err) w.WriteHeader(http.StatusBadRequest) _, _ = w.Write(jsonState) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } _, err := api.GetDeviceCode(context.Background(), "bad_audience") assert.ErrorContains(t, err, "invalid audience") }) t.Run("general error", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "an error", http.StatusInternalServerError) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } _, err := api.GetDeviceCode(context.Background(), "anAudience") assert.ErrorContains(t, err, "unexpected response from tenant: 500 Internal Server Error") }) t.Run("canceled context", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { time.Sleep(2 * time.Second) http.Error(w, "an error", http.StatusInternalServerError) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(1 * time.Second) cancel() }() _, err := api.GetDeviceCode(ctx, "anAudience") assert.ErrorContains(t, err, "context canceled") }) } func TestWaitForDeviceToken(t *testing.T) { t.Parallel() t.Run("success", func(t *testing.T) { t.Parallel() expectedToken := TokenResponse{ AccessToken: "a-real-token", IDToken: "", RefreshToken: "the-refresh-token", Scope: "", ExpiresIn: 3600, TokenType: "", } var respond atomic.Bool go func() { time.Sleep(5 * time.Second) respond.Store(true) }() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/oauth/token", r.URL.Path) assert.Equal(t, r.FormValue("client_id"), "aClientID") assert.Equal(t, r.FormValue("grant_type"), "urn:ietf:params:oauth:grant-type:device_code") assert.Equal(t, r.FormValue("device_code"), "aDeviceCode") if respond.Load() { jsonState, err := json.Marshal(expectedToken) assert.NilError(t, err) w.Write(jsonState) } else { pendingError := "authorization_pending" jsonResponse, err := json.Marshal(TokenResponse{ Error: &pendingError, }) assert.NilError(t, err) w.Write(jsonResponse) } })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } state := State{ DeviceCode: "aDeviceCode", UserCode: "aUserCode", Interval: 1, ExpiresIn: 30, } token, err := api.WaitForDeviceToken(context.Background(), state) assert.NilError(t, err) assert.DeepEqual(t, token, expectedToken) }) t.Run("timeout", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/oauth/token", r.URL.Path) assert.Equal(t, r.FormValue("client_id"), "aClientID") assert.Equal(t, r.FormValue("grant_type"), "urn:ietf:params:oauth:grant-type:device_code") assert.Equal(t, r.FormValue("device_code"), "aDeviceCode") pendingError := "authorization_pending" jsonResponse, err := json.Marshal(TokenResponse{ Error: &pendingError, }) assert.NilError(t, err) w.Write(jsonResponse) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } state := State{ DeviceCode: "aDeviceCode", UserCode: "aUserCode", Interval: 1, ExpiresIn: 1, } _, err := api.WaitForDeviceToken(context.Background(), state) assert.ErrorIs(t, err, ErrTimeout) }) t.Run("canceled context", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { pendingError := "authorization_pending" jsonResponse, err := json.Marshal(TokenResponse{ Error: &pendingError, }) assert.NilError(t, err) w.Write(jsonResponse) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } state := State{ DeviceCode: "aDeviceCode", UserCode: "aUserCode", Interval: 1, ExpiresIn: 5, } ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(1 * time.Second) cancel() }() _, err := api.WaitForDeviceToken(ctx, state) assert.ErrorContains(t, err, "context canceled") }) } func TestRevoke(t *testing.T) { t.Parallel() t.Run("success", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/oauth/revoke", r.URL.Path) assert.Equal(t, r.FormValue("client_id"), "aClientID") assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token") w.WriteHeader(http.StatusOK) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } err := api.RevokeToken(context.Background(), "v1.a-refresh-token") assert.NilError(t, err) }) t.Run("unexpected response", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/oauth/revoke", r.URL.Path) assert.Equal(t, r.FormValue("client_id"), "aClientID") assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token") w.WriteHeader(http.StatusNotFound) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } err := api.RevokeToken(context.Background(), "v1.a-refresh-token") assert.ErrorContains(t, err, "unexpected response from tenant: 404 Not Found") }) t.Run("error w/ description", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { jsonState, err := json.Marshal(TokenResponse{ ErrorDescription: "invalid client id", }) assert.NilError(t, err) w.WriteHeader(http.StatusBadRequest) _, _ = w.Write(jsonState) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } err := api.RevokeToken(context.Background(), "v1.a-refresh-token") assert.ErrorContains(t, err, "invalid client id") }) t.Run("canceled context", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/oauth/revoke", r.URL.Path) assert.Equal(t, r.FormValue("client_id"), "aClientID") assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token") w.WriteHeader(http.StatusOK) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } ctx, cancel := context.WithCancel(context.Background()) cancel() err := api.RevokeToken(ctx, "v1.a-refresh-token") assert.ErrorContains(t, err, "context canceled") }) } func TestGetAutoPAT(t *testing.T) { t.Parallel() t.Run("success", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path) assert.Equal(t, "Bearer bork", r.Header.Get("Authorization")) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) marshalledResponse, err := json.Marshal(patGenerateResponse{ Data: struct { Token string `json:"token"` }{ Token: "a-docker-pat", }, }) assert.NilError(t, err) w.WriteHeader(http.StatusCreated) w.Write(marshalledResponse) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } pat, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{ AccessToken: "bork", }) assert.NilError(t, err) assert.Equal(t, "a-docker-pat", pat) }) t.Run("general error", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } _, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{ AccessToken: "bork", }) assert.ErrorContains(t, err, "unexpected response from Hub: 500 Internal Server Error") }) t.Run("context canceled", func(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path) assert.Equal(t, "Bearer bork", r.Header.Get("Authorization")) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) marshalledResponse, err := json.Marshal(patGenerateResponse{ Data: struct { Token string `json:"token"` }{ Token: "a-docker-pat", }, }) assert.NilError(t, err) time.Sleep(500 * time.Millisecond) w.WriteHeader(http.StatusCreated) w.Write(marshalledResponse) })) defer ts.Close() api := API{ TenantURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, } ctx, cancel := context.WithCancel(context.Background()) cancel() pat, err := api.GetAutoPAT(ctx, ts.URL, TokenResponse{ AccessToken: "bork", }) assert.ErrorContains(t, err, "context canceled") assert.Equal(t, "", pat) }) }