| 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: 5, | |
| 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) | |
| }) | |
| } |