1package cli
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "github.com/bnema/gordon/internal/adapters/dto"
14 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
15)
16
17// TestExchangeRegistryAuth_Success verifies the happy path:
18// VerifyAuth is called, then ExchangeRegistryToken is called with the
19// subject from VerifyAuth, and the result is "Bearer <short-lived-token>".
20func TestExchangeRegistryAuth_Success(t *testing.T) {
21 const subject = "ci-bot"
22 const shortToken = "short-lived-token-abc123"
23
24 verifyHandled := false
25 tokenHandled := false
26
27 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28 switch r.URL.Path {
29 case "/admin/auth/verify":
30 verifyHandled = true
31 w.Header().Set("Content-Type", "application/json")
32 resp := dto.AuthVerifyResponse{
33 Valid: true,
34 Subject: subject,
35 Scopes: []string{"push", "pull"},
36 }
37 json.NewEncoder(w).Encode(resp) //nolint:errcheck
38 case "/auth/token":
39 tokenHandled = true
40 // Verify basic auth was set with subject as username
41 u, p, ok := r.BasicAuth()
42 assert.True(t, ok, "expected basic auth")
43 assert.Equal(t, subject, u, "expected subject as username")
44 assert.NotEmpty(t, p, "expected token as password")
45
46 w.Header().Set("Content-Type", "application/json")
47 resp := dto.TokenResponse{Token: shortToken}
48 json.NewEncoder(w).Encode(resp) //nolint:errcheck
49 default:
50 http.NotFound(w, r)
51 }
52 }))
53 defer srv.Close()
54
55 client := remote.NewClient(srv.URL, remote.WithToken("long-lived-token"))
56 ops := &dockerImageOps{
57 remoteClient: client,
58 }
59
60 got, err := ops.exchangeRegistryAuth(context.Background())
61
62 require.NoError(t, err)
63 assert.Equal(t, "Bearer "+shortToken, got)
64 assert.True(t, verifyHandled, "expected /admin/auth/verify to be called")
65 assert.True(t, tokenHandled, "expected /auth/token to be called")
66}
67
68// TestExchangeRegistryAuth_VerifyAuthFails verifies that when VerifyAuth fails,
69// exchangeRegistryAuth returns an error.
70func TestExchangeRegistryAuth_VerifyAuthFails(t *testing.T) {
71 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
72 if r.URL.Path == "/admin/auth/verify" {
73 http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
74 return
75 }
76 http.NotFound(w, r)
77 }))
78 defer srv.Close()
79
80 client := remote.NewClient(srv.URL, remote.WithToken("bad-token"))
81 ops := &dockerImageOps{
82 remoteClient: client,
83 }
84
85 _, err := ops.exchangeRegistryAuth(context.Background())
86
87 require.Error(t, err)
88 assert.Contains(t, err.Error(), "failed to verify token")
89}
90
91// TestExchangeRegistryAuth_TokenNotValid verifies that when VerifyAuth returns
92// Valid=false, exchangeRegistryAuth returns an error.
93func TestExchangeRegistryAuth_TokenNotValid(t *testing.T) {
94 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
95 if r.URL.Path == "/admin/auth/verify" {
96 w.Header().Set("Content-Type", "application/json")
97 resp := dto.AuthVerifyResponse{Valid: false}
98 json.NewEncoder(w).Encode(resp) //nolint:errcheck
99 return
100 }
101 http.NotFound(w, r)
102 }))
103 defer srv.Close()
104
105 client := remote.NewClient(srv.URL, remote.WithToken("expired-token"))
106 ops := &dockerImageOps{
107 remoteClient: client,
108 }
109
110 _, err := ops.exchangeRegistryAuth(context.Background())
111
112 require.Error(t, err)
113 assert.Contains(t, err.Error(), "token is not valid")
114}