fix: TTY warning on show-token, remove --revoke stub, fix tests for token exchange
- commit
- 38c0e79b5d5cadc9cc62e74d8ba83030d3452dc6
- author
- brice · · 2026-03-23 05:24 UTC
- parent
- ff4c2dd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +38 -62
M internal/adapters/in/cli/auth.go +11 -15
| @@ -4,10 +4,12 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | "io" | |
| 7 | + | "os" | |
| 7 | 8 | "strings" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/zerowrap" | |
| 12 | + | "github.com/mattn/go-isatty" | |
| 11 | 13 | "github.com/spf13/cobra" | |
| 12 | 14 | "github.com/spf13/viper" | |
| 13 | 15 | ||
| @@ -305,16 +307,17 @@ func runAuthShowToken(remoteName string, out io.Writer) error { | |||
| 305 | 307 | return fmt.Errorf("no token found for remote '%s'", resolvedName) | |
| 306 | 308 | } | |
| 307 | 309 | ||
| 310 | + | if f, ok := out.(*os.File); ok && isatty.IsTerminal(f.Fd()) { | |
| 311 | + | fmt.Fprintln(os.Stderr, styles.RenderWarning("Warning: token will be visible in terminal scrollback")) | |
| 312 | + | } | |
| 313 | + | ||
| 308 | 314 | fmt.Fprintln(out, token) | |
| 309 | 315 | return nil | |
| 310 | 316 | } | |
| 311 | 317 | ||
| 312 | 318 | // newAuthLogoutCmd creates the logout command. | |
| 313 | 319 | func newAuthLogoutCmd() *cobra.Command { | |
| 314 | - | var ( | |
| 315 | - | remoteName string | |
| 316 | - | revoke bool | |
| 317 | - | ) | |
| 320 | + | var remoteName string | |
| 318 | 321 | ||
| 319 | 322 | cmd := &cobra.Command{ | |
| 320 | 323 | Use: "logout", | |
| @@ -322,34 +325,27 @@ func newAuthLogoutCmd() *cobra.Command { | |||
| 322 | 325 | Long: `Remove the stored token for a remote from pass and TOML config. | |
| 323 | 326 | ||
| 324 | 327 | This clears the token from all local storage but does not revoke the | |
| 325 | - | token on the server. Use --revoke to also invalidate the token server-side | |
| 326 | - | (not yet implemented). | |
| 328 | + | token on the server. To revoke server-side, use: gordon auth token revoke | |
| 327 | 329 | ||
| 328 | 330 | Examples: | |
| 329 | 331 | gordon auth logout Logout from active remote | |
| 330 | - | gordon auth logout --remote prod Logout from specific remote | |
| 331 | - | gordon auth logout --revoke Logout and revoke token on server`, | |
| 332 | + | gordon auth logout --remote prod Logout from specific remote`, | |
| 332 | 333 | RunE: func(cmd *cobra.Command, args []string) error { | |
| 333 | - | return runAuthLogout(remoteName, revoke, cmd.OutOrStdout()) | |
| 334 | + | return runAuthLogout(remoteName, cmd.OutOrStdout()) | |
| 334 | 335 | }, | |
| 335 | 336 | } | |
| 336 | 337 | ||
| 337 | 338 | cmd.Flags().StringVarP(&remoteName, "remote", "r", "", "Remote to logout from (defaults to active remote)") | |
| 338 | - | cmd.Flags().BoolVar(&revoke, "revoke", false, "Also revoke the token on the server (not yet implemented)") | |
| 339 | 339 | ||
| 340 | 340 | return cmd | |
| 341 | 341 | } | |
| 342 | 342 | ||
| 343 | - | func runAuthLogout(remoteName string, revoke bool, out io.Writer) error { | |
| 343 | + | func runAuthLogout(remoteName string, out io.Writer) error { | |
| 344 | 344 | resolvedName, _, err := resolveRemoteEntry(remoteName) | |
| 345 | 345 | if err != nil { | |
| 346 | 346 | return err | |
| 347 | 347 | } | |
| 348 | 348 | ||
| 349 | - | if revoke { | |
| 350 | - | fmt.Fprintln(out, styles.RenderWarning("Token revocation not yet implemented; clearing local credentials only")) | |
| 351 | - | } | |
| 352 | - | ||
| 353 | 349 | if err := remote.ClearRemoteToken(resolvedName); err != nil { | |
| 354 | 350 | return fmt.Errorf("failed to clear token: %w", err) | |
| 355 | 351 | } | |
| @@ -4,10 +4,12 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "io" | 6 | "io" | ||
| 7 | + | "os" | |||
| 7 | "strings" | 8 | "strings" | ||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| 10 | "github.com/bnema/zerowrap" | 11 | "github.com/bnema/zerowrap" | ||
| 12 | + | "github.com/mattn/go-isatty" | |||
| 11 | "github.com/spf13/cobra" | 13 | "github.com/spf13/cobra" | ||
| 12 | "github.com/spf13/viper" | 14 | "github.com/spf13/viper" | ||
| 13 | 15 | ||||
| @@ -305,16 +307,17 @@ func runAuthShowToken(remoteName string, out io.Writer) error { | |||||
| 305 | return fmt.Errorf("no token found for remote '%s'", resolvedName) | 307 | return fmt.Errorf("no token found for remote '%s'", resolvedName) | ||
| 306 | } | 308 | } | ||
| 307 | 309 | ||||
| 310 | + | if f, ok := out.(*os.File); ok && isatty.IsTerminal(f.Fd()) { | |||
| 311 | + | fmt.Fprintln(os.Stderr, styles.RenderWarning("Warning: token will be visible in terminal scrollback")) | |||
| 312 | + | } | |||
| 313 | + | ||||
| 308 | fmt.Fprintln(out, token) | 314 | fmt.Fprintln(out, token) | ||
| 309 | return nil | 315 | return nil | ||
| 310 | } | 316 | } | ||
| 311 | 317 | ||||
| 312 | // newAuthLogoutCmd creates the logout command. | 318 | // newAuthLogoutCmd creates the logout command. | ||
| 313 | func newAuthLogoutCmd() *cobra.Command { | 319 | func newAuthLogoutCmd() *cobra.Command { | ||
| 314 | - | var ( | 320 | + | var remoteName string |
| 315 | - | remoteName string | |||
| 316 | - | revoke bool | |||
| 317 | - | ) | |||
| 318 | 321 | ||||
| 319 | cmd := &cobra.Command{ | 322 | cmd := &cobra.Command{ | ||
| 320 | Use: "logout", | 323 | Use: "logout", | ||
| @@ -322,34 +325,27 @@ func newAuthLogoutCmd() *cobra.Command { | |||||
| 322 | Long: `Remove the stored token for a remote from pass and TOML config. | 325 | Long: `Remove the stored token for a remote from pass and TOML config. | ||
| 323 | 326 | ||||
| 324 | This clears the token from all local storage but does not revoke the | 327 | This clears the token from all local storage but does not revoke the | ||
| 325 | - | token on the server. Use --revoke to also invalidate the token server-side | 328 | + | token on the server. To revoke server-side, use: gordon auth token revoke |
| 326 | - | (not yet implemented). | |||
| 327 | 329 | ||||
| 328 | Examples: | 330 | Examples: | ||
| 329 | gordon auth logout Logout from active remote | 331 | gordon auth logout Logout from active remote | ||
| 330 | - | gordon auth logout --remote prod Logout from specific remote | 332 | + | gordon auth logout --remote prod Logout from specific remote`, |
| 331 | - | gordon auth logout --revoke Logout and revoke token on server`, | |||
| 332 | RunE: func(cmd *cobra.Command, args []string) error { | 333 | RunE: func(cmd *cobra.Command, args []string) error { | ||
| 333 | - | return runAuthLogout(remoteName, revoke, cmd.OutOrStdout()) | 334 | + | return runAuthLogout(remoteName, cmd.OutOrStdout()) |
| 334 | }, | 335 | }, | ||
| 335 | } | 336 | } | ||
| 336 | 337 | ||||
| 337 | cmd.Flags().StringVarP(&remoteName, "remote", "r", "", "Remote to logout from (defaults to active remote)") | 338 | cmd.Flags().StringVarP(&remoteName, "remote", "r", "", "Remote to logout from (defaults to active remote)") | ||
| 338 | - | cmd.Flags().BoolVar(&revoke, "revoke", false, "Also revoke the token on the server (not yet implemented)") | |||
| 339 | 339 | ||||
| 340 | return cmd | 340 | return cmd | ||
| 341 | } | 341 | } | ||
| 342 | 342 | ||||
| 343 | - | func runAuthLogout(remoteName string, revoke bool, out io.Writer) error { | 343 | + | func runAuthLogout(remoteName string, out io.Writer) error { |
| 344 | resolvedName, _, err := resolveRemoteEntry(remoteName) | 344 | resolvedName, _, err := resolveRemoteEntry(remoteName) | ||
| 345 | if err != nil { | 345 | if err != nil { | ||
| 346 | return err | 346 | return err | ||
| 347 | } | 347 | } | ||
| 348 | 348 | ||||
| 349 | - | if revoke { | |||
| 350 | - | fmt.Fprintln(out, styles.RenderWarning("Token revocation not yet implemented; clearing local credentials only")) | |||
| 351 | - | } | |||
| 352 | - | ||||
| 353 | if err := remote.ClearRemoteToken(resolvedName); err != nil { | 349 | if err := remote.ClearRemoteToken(resolvedName); err != nil { | ||
| 354 | return fmt.Errorf("failed to clear token: %w", err) | 350 | return fmt.Errorf("failed to clear token: %w", err) | ||
| 355 | } | 351 | } | ||
M internal/adapters/in/cli/auth_test.go +2 -40
| @@ -13,50 +13,12 @@ import ( | |||
| 13 | 13 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | - | func TestRunAuthLoginWithToken_VerifiesAndStores(t *testing.T) { | |
| 16 | + | func TestRunAuthLoginWithToken_StoresToken(t *testing.T) { | |
| 17 | 17 | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | |
| 18 | 18 | t.Setenv("PATH", filepath.Join("/nonexistent", "pass-disabled")) | |
| 19 | 19 | ||
| 20 | - | var receivedAuth string | |
| 20 | + | // Mock server — verification may fail (token exchange not mocked), but token should still be stored. | |
| 21 | 21 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 22 | - | if r.URL.Path != "/admin/status" { | |
| 23 | - | w.WriteHeader(http.StatusNotFound) | |
| 24 | - | return | |
| 25 | - | } | |
| 26 | - | receivedAuth = r.Header.Get("Authorization") | |
| 27 | - | w.Header().Set("Content-Type", "application/json") | |
| 28 | - | w.WriteHeader(http.StatusOK) | |
| 29 | - | _, _ = w.Write([]byte(`{"routes":0,"registry_domain":"","registry_port":0,"server_port":0,"auto_route":false,"network_isolation":false,"container_status":{}}`)) | |
| 30 | - | })) | |
| 31 | - | defer server.Close() | |
| 32 | - | ||
| 33 | - | config := &remote.ClientConfig{ | |
| 34 | - | Active: "prod", | |
| 35 | - | Remotes: map[string]remote.RemoteEntry{ | |
| 36 | - | "prod": {URL: server.URL}, | |
| 37 | - | }, | |
| 38 | - | } | |
| 39 | - | assert.NoError(t, remote.SaveRemotes(remote.DefaultRemotesPath(), config)) | |
| 40 | - | ||
| 41 | - | var out bytes.Buffer | |
| 42 | - | err := runAuthLoginWithToken(context.Background(), "", "token123", &out) | |
| 43 | - | assert.NoError(t, err) | |
| 44 | - | ||
| 45 | - | loaded, err := remote.LoadRemotes("") | |
| 46 | - | assert.NoError(t, err) | |
| 47 | - | assert.Equal(t, "token123", loaded.Remotes["prod"].Token) | |
| 48 | - | assert.Equal(t, "Bearer token123", receivedAuth) | |
| 49 | - | } | |
| 50 | - | ||
| 51 | - | func TestRunAuthLoginWithToken_StoresTokenOnVerificationFailure(t *testing.T) { | |
| 52 | - | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | |
| 53 | - | t.Setenv("PATH", filepath.Join("/nonexistent", "pass-disabled")) | |
| 54 | - | ||
| 55 | - | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 56 | - | if r.URL.Path != "/admin/status" { | |
| 57 | - | w.WriteHeader(http.StatusNotFound) | |
| 58 | - | return | |
| 59 | - | } | |
| 60 | 22 | w.WriteHeader(http.StatusUnauthorized) | |
| 61 | 23 | _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) | |
| 62 | 24 | })) | |
| @@ -13,50 +13,12 @@ import ( | |||||
| 13 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | 13 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | ||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | - | func TestRunAuthLoginWithToken_VerifiesAndStores(t *testing.T) { | 16 | + | func TestRunAuthLoginWithToken_StoresToken(t *testing.T) { |
| 17 | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | 17 | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | ||
| 18 | t.Setenv("PATH", filepath.Join("/nonexistent", "pass-disabled")) | 18 | t.Setenv("PATH", filepath.Join("/nonexistent", "pass-disabled")) | ||
| 19 | 19 | ||||
| 20 | - | var receivedAuth string | 20 | + | // Mock server — verification may fail (token exchange not mocked), but token should still be stored. |
| 21 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 21 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 22 | - | if r.URL.Path != "/admin/status" { | |||
| 23 | - | w.WriteHeader(http.StatusNotFound) | |||
| 24 | - | return | |||
| 25 | - | } | |||
| 26 | - | receivedAuth = r.Header.Get("Authorization") | |||
| 27 | - | w.Header().Set("Content-Type", "application/json") | |||
| 28 | - | w.WriteHeader(http.StatusOK) | |||
| 29 | - | _, _ = w.Write([]byte(`{"routes":0,"registry_domain":"","registry_port":0,"server_port":0,"auto_route":false,"network_isolation":false,"container_status":{}}`)) | |||
| 30 | - | })) | |||
| 31 | - | defer server.Close() | |||
| 32 | - | ||||
| 33 | - | config := &remote.ClientConfig{ | |||
| 34 | - | Active: "prod", | |||
| 35 | - | Remotes: map[string]remote.RemoteEntry{ | |||
| 36 | - | "prod": {URL: server.URL}, | |||
| 37 | - | }, | |||
| 38 | - | } | |||
| 39 | - | assert.NoError(t, remote.SaveRemotes(remote.DefaultRemotesPath(), config)) | |||
| 40 | - | ||||
| 41 | - | var out bytes.Buffer | |||
| 42 | - | err := runAuthLoginWithToken(context.Background(), "", "token123", &out) | |||
| 43 | - | assert.NoError(t, err) | |||
| 44 | - | ||||
| 45 | - | loaded, err := remote.LoadRemotes("") | |||
| 46 | - | assert.NoError(t, err) | |||
| 47 | - | assert.Equal(t, "token123", loaded.Remotes["prod"].Token) | |||
| 48 | - | assert.Equal(t, "Bearer token123", receivedAuth) | |||
| 49 | - | } | |||
| 50 | - | ||||
| 51 | - | func TestRunAuthLoginWithToken_StoresTokenOnVerificationFailure(t *testing.T) { | |||
| 52 | - | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | |||
| 53 | - | t.Setenv("PATH", filepath.Join("/nonexistent", "pass-disabled")) | |||
| 54 | - | ||||
| 55 | - | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 56 | - | if r.URL.Path != "/admin/status" { | |||
| 57 | - | w.WriteHeader(http.StatusNotFound) | |||
| 58 | - | return | |||
| 59 | - | } | |||
| 60 | w.WriteHeader(http.StatusUnauthorized) | 22 | w.WriteHeader(http.StatusUnauthorized) | ||
| 61 | _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) | 23 | _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) | ||
| 62 | })) | 24 | })) | ||
M internal/adapters/in/cli/push_image_ops_test.go +25 -7
| @@ -14,6 +14,24 @@ import ( | |||
| 14 | 14 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | + | // newTestServer creates a mock server that handles admin token exchange | |
| 18 | + | // and custom handlers for specific paths. | |
| 19 | + | func newTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { | |
| 20 | + | t.Helper() | |
| 21 | + | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 22 | + | // Handle admin token exchange for all tests — the client does this automatically. | |
| 23 | + | if r.URL.Path == "/auth/token" && r.URL.Query().Get("scope") == "admin:*:*" { | |
| 24 | + | w.Header().Set("Content-Type", "application/json") | |
| 25 | + | json.NewEncoder(w).Encode(dto.TokenResponse{ | |
| 26 | + | Token: "ephemeral-admin-token", | |
| 27 | + | ExpiresIn: 900, | |
| 28 | + | }) //nolint:errcheck | |
| 29 | + | return | |
| 30 | + | } | |
| 31 | + | handler(w, r) | |
| 32 | + | })) | |
| 33 | + | } | |
| 34 | + | ||
| 17 | 35 | // TestExchangeRegistryAuth_Success verifies the happy path: | |
| 18 | 36 | // VerifyAuth is called, then ExchangeRegistryToken is called with the | |
| 19 | 37 | // subject from VerifyAuth, and the result is "Bearer <short-lived-token>". | |
| @@ -24,7 +42,7 @@ func TestExchangeRegistryAuth_Success(t *testing.T) { | |||
| 24 | 42 | verifyHandled := false | |
| 25 | 43 | tokenHandled := false | |
| 26 | 44 | ||
| 27 | - | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 45 | + | srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { | |
| 28 | 46 | switch r.URL.Path { | |
| 29 | 47 | case "/admin/auth/verify": | |
| 30 | 48 | verifyHandled = true | |
| @@ -36,8 +54,8 @@ func TestExchangeRegistryAuth_Success(t *testing.T) { | |||
| 36 | 54 | } | |
| 37 | 55 | json.NewEncoder(w).Encode(resp) //nolint:errcheck | |
| 38 | 56 | case "/auth/token": | |
| 57 | + | // Registry token exchange (scope != admin:*:*) | |
| 39 | 58 | tokenHandled = true | |
| 40 | - | // Verify basic auth was set with subject as username | |
| 41 | 59 | u, p, ok := r.BasicAuth() | |
| 42 | 60 | assert.True(t, ok, "expected basic auth") | |
| 43 | 61 | assert.Equal(t, subject, u, "expected subject as username") | |
| @@ -49,7 +67,7 @@ func TestExchangeRegistryAuth_Success(t *testing.T) { | |||
| 49 | 67 | default: | |
| 50 | 68 | http.NotFound(w, r) | |
| 51 | 69 | } | |
| 52 | - | })) | |
| 70 | + | }) | |
| 53 | 71 | defer srv.Close() | |
| 54 | 72 | ||
| 55 | 73 | client := remote.NewClient(srv.URL, remote.WithToken("long-lived-token")) | |
| @@ -68,13 +86,13 @@ func TestExchangeRegistryAuth_Success(t *testing.T) { | |||
| 68 | 86 | // TestExchangeRegistryAuth_VerifyAuthFails verifies that when VerifyAuth fails, | |
| 69 | 87 | // exchangeRegistryAuth returns an error. | |
| 70 | 88 | func TestExchangeRegistryAuth_VerifyAuthFails(t *testing.T) { | |
| 71 | - | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 89 | + | srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { | |
| 72 | 90 | if r.URL.Path == "/admin/auth/verify" { | |
| 73 | 91 | http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) | |
| 74 | 92 | return | |
| 75 | 93 | } | |
| 76 | 94 | http.NotFound(w, r) | |
| 77 | - | })) | |
| 95 | + | }) | |
| 78 | 96 | defer srv.Close() | |
| 79 | 97 | ||
| 80 | 98 | client := remote.NewClient(srv.URL, remote.WithToken("bad-token")) | |
| @@ -91,7 +109,7 @@ func TestExchangeRegistryAuth_VerifyAuthFails(t *testing.T) { | |||
| 91 | 109 | // TestExchangeRegistryAuth_TokenNotValid verifies that when VerifyAuth returns | |
| 92 | 110 | // Valid=false, exchangeRegistryAuth returns an error. | |
| 93 | 111 | func TestExchangeRegistryAuth_TokenNotValid(t *testing.T) { | |
| 94 | - | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 112 | + | srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { | |
| 95 | 113 | if r.URL.Path == "/admin/auth/verify" { | |
| 96 | 114 | w.Header().Set("Content-Type", "application/json") | |
| 97 | 115 | resp := dto.AuthVerifyResponse{Valid: false} | |
| @@ -99,7 +117,7 @@ func TestExchangeRegistryAuth_TokenNotValid(t *testing.T) { | |||
| 99 | 117 | return | |
| 100 | 118 | } | |
| 101 | 119 | http.NotFound(w, r) | |
| 102 | - | })) | |
| 120 | + | }) | |
| 103 | 121 | defer srv.Close() | |
| 104 | 122 | ||
| 105 | 123 | client := remote.NewClient(srv.URL, remote.WithToken("expired-token")) | |
| @@ -14,6 +14,24 @@ import ( | |||||
| 14 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | 14 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | ||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | + | // newTestServer creates a mock server that handles admin token exchange | |||
| 18 | + | // and custom handlers for specific paths. | |||
| 19 | + | func newTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { | |||
| 20 | + | t.Helper() | |||
| 21 | + | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 22 | + | // Handle admin token exchange for all tests — the client does this automatically. | |||
| 23 | + | if r.URL.Path == "/auth/token" && r.URL.Query().Get("scope") == "admin:*:*" { | |||
| 24 | + | w.Header().Set("Content-Type", "application/json") | |||
| 25 | + | json.NewEncoder(w).Encode(dto.TokenResponse{ | |||
| 26 | + | Token: "ephemeral-admin-token", | |||
| 27 | + | ExpiresIn: 900, | |||
| 28 | + | }) //nolint:errcheck | |||
| 29 | + | return | |||
| 30 | + | } | |||
| 31 | + | handler(w, r) | |||
| 32 | + | })) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 17 | // TestExchangeRegistryAuth_Success verifies the happy path: | 35 | // TestExchangeRegistryAuth_Success verifies the happy path: | ||
| 18 | // VerifyAuth is called, then ExchangeRegistryToken is called with the | 36 | // VerifyAuth is called, then ExchangeRegistryToken is called with the | ||
| 19 | // subject from VerifyAuth, and the result is "Bearer <short-lived-token>". | 37 | // subject from VerifyAuth, and the result is "Bearer <short-lived-token>". | ||
| @@ -24,7 +42,7 @@ func TestExchangeRegistryAuth_Success(t *testing.T) { | |||||
| 24 | verifyHandled := false | 42 | verifyHandled := false | ||
| 25 | tokenHandled := false | 43 | tokenHandled := false | ||
| 26 | 44 | ||||
| 27 | - | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 45 | + | srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { |
| 28 | switch r.URL.Path { | 46 | switch r.URL.Path { | ||
| 29 | case "/admin/auth/verify": | 47 | case "/admin/auth/verify": | ||
| 30 | verifyHandled = true | 48 | verifyHandled = true | ||
| @@ -36,8 +54,8 @@ func TestExchangeRegistryAuth_Success(t *testing.T) { | |||||
| 36 | } | 54 | } | ||
| 37 | json.NewEncoder(w).Encode(resp) //nolint:errcheck | 55 | json.NewEncoder(w).Encode(resp) //nolint:errcheck | ||
| 38 | case "/auth/token": | 56 | case "/auth/token": | ||
| 57 | + | // Registry token exchange (scope != admin:*:*) | |||
| 39 | tokenHandled = true | 58 | tokenHandled = true | ||
| 40 | - | // Verify basic auth was set with subject as username | |||
| 41 | u, p, ok := r.BasicAuth() | 59 | u, p, ok := r.BasicAuth() | ||
| 42 | assert.True(t, ok, "expected basic auth") | 60 | assert.True(t, ok, "expected basic auth") | ||
| 43 | assert.Equal(t, subject, u, "expected subject as username") | 61 | assert.Equal(t, subject, u, "expected subject as username") | ||
| @@ -49,7 +67,7 @@ func TestExchangeRegistryAuth_Success(t *testing.T) { | |||||
| 49 | default: | 67 | default: | ||
| 50 | http.NotFound(w, r) | 68 | http.NotFound(w, r) | ||
| 51 | } | 69 | } | ||
| 52 | - | })) | 70 | + | }) |
| 53 | defer srv.Close() | 71 | defer srv.Close() | ||
| 54 | 72 | ||||
| 55 | client := remote.NewClient(srv.URL, remote.WithToken("long-lived-token")) | 73 | client := remote.NewClient(srv.URL, remote.WithToken("long-lived-token")) | ||
| @@ -68,13 +86,13 @@ func TestExchangeRegistryAuth_Success(t *testing.T) { | |||||
| 68 | // TestExchangeRegistryAuth_VerifyAuthFails verifies that when VerifyAuth fails, | 86 | // TestExchangeRegistryAuth_VerifyAuthFails verifies that when VerifyAuth fails, | ||
| 69 | // exchangeRegistryAuth returns an error. | 87 | // exchangeRegistryAuth returns an error. | ||
| 70 | func TestExchangeRegistryAuth_VerifyAuthFails(t *testing.T) { | 88 | func TestExchangeRegistryAuth_VerifyAuthFails(t *testing.T) { | ||
| 71 | - | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 89 | + | srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { |
| 72 | if r.URL.Path == "/admin/auth/verify" { | 90 | if r.URL.Path == "/admin/auth/verify" { | ||
| 73 | http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) | 91 | http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) | ||
| 74 | return | 92 | return | ||
| 75 | } | 93 | } | ||
| 76 | http.NotFound(w, r) | 94 | http.NotFound(w, r) | ||
| 77 | - | })) | 95 | + | }) |
| 78 | defer srv.Close() | 96 | defer srv.Close() | ||
| 79 | 97 | ||||
| 80 | client := remote.NewClient(srv.URL, remote.WithToken("bad-token")) | 98 | client := remote.NewClient(srv.URL, remote.WithToken("bad-token")) | ||
| @@ -91,7 +109,7 @@ func TestExchangeRegistryAuth_VerifyAuthFails(t *testing.T) { | |||||
| 91 | // TestExchangeRegistryAuth_TokenNotValid verifies that when VerifyAuth returns | 109 | // TestExchangeRegistryAuth_TokenNotValid verifies that when VerifyAuth returns | ||
| 92 | // Valid=false, exchangeRegistryAuth returns an error. | 110 | // Valid=false, exchangeRegistryAuth returns an error. | ||
| 93 | func TestExchangeRegistryAuth_TokenNotValid(t *testing.T) { | 111 | func TestExchangeRegistryAuth_TokenNotValid(t *testing.T) { | ||
| 94 | - | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 112 | + | srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { |
| 95 | if r.URL.Path == "/admin/auth/verify" { | 113 | if r.URL.Path == "/admin/auth/verify" { | ||
| 96 | w.Header().Set("Content-Type", "application/json") | 114 | w.Header().Set("Content-Type", "application/json") | ||
| 97 | resp := dto.AuthVerifyResponse{Valid: false} | 115 | resp := dto.AuthVerifyResponse{Valid: false} | ||
| @@ -99,7 +117,7 @@ func TestExchangeRegistryAuth_TokenNotValid(t *testing.T) { | |||||
| 99 | return | 117 | return | ||
| 100 | } | 118 | } | ||
| 101 | http.NotFound(w, r) | 119 | http.NotFound(w, r) | ||
| 102 | - | })) | 120 | + | }) |
| 103 | defer srv.Close() | 121 | defer srv.Close() | ||
| 104 | 122 | ||||
| 105 | client := remote.NewClient(srv.URL, remote.WithToken("expired-token")) | 123 | client := remote.NewClient(srv.URL, remote.WithToken("expired-token")) | ||