v3.4.1 de8f68e

push_image_ops_test.go RawHistoryFolder

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// newTestServer creates a mock server that handles admin token exchange
18// and custom handlers for specific paths.
19func 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
35// TestExchangeRegistryAuth_Success verifies the happy path:
36// VerifyAuth is called, then ExchangeRegistryToken is called with the
37// subject from VerifyAuth, and the result is "Bearer <short-lived-token>".
38func TestExchangeRegistryAuth_Success(t *testing.T) {
39	const subject = "ci-bot"
40	const shortToken = "short-lived-token-abc123"
41
42	verifyHandled := false
43	tokenHandled := false
44
45	srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
46		switch r.URL.Path {
47		case "/admin/auth/verify":
48			verifyHandled = true
49			w.Header().Set("Content-Type", "application/json")
50			resp := dto.AuthVerifyResponse{
51				Valid:   true,
52				Subject: subject,
53				Scopes:  []string{"push", "pull"},
54			}
55			json.NewEncoder(w).Encode(resp) //nolint:errcheck
56		case "/auth/token":
57			// Registry token exchange (scope != admin:*:*)
58			tokenHandled = true
59			u, p, ok := r.BasicAuth()
60			assert.True(t, ok, "expected basic auth")
61			assert.Equal(t, subject, u, "expected subject as username")
62			assert.NotEmpty(t, p, "expected token as password")
63
64			w.Header().Set("Content-Type", "application/json")
65			resp := dto.TokenResponse{Token: shortToken}
66			json.NewEncoder(w).Encode(resp) //nolint:errcheck
67		default:
68			http.NotFound(w, r)
69		}
70	})
71	defer srv.Close()
72
73	client := remote.NewClient(srv.URL, remote.WithToken("long-lived-token"))
74	ops := &dockerImageOps{
75		remoteClient: client,
76	}
77
78	got, err := ops.exchangeRegistryAuth(context.Background())
79
80	require.NoError(t, err)
81	assert.Equal(t, "Bearer "+shortToken, got)
82	assert.True(t, verifyHandled, "expected /admin/auth/verify to be called")
83	assert.True(t, tokenHandled, "expected /auth/token to be called")
84}
85
86// TestExchangeRegistryAuth_VerifyAuthFails verifies that when VerifyAuth fails,
87// exchangeRegistryAuth returns an error.
88func TestExchangeRegistryAuth_VerifyAuthFails(t *testing.T) {
89	srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
90		if r.URL.Path == "/admin/auth/verify" {
91			http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
92			return
93		}
94		http.NotFound(w, r)
95	})
96	defer srv.Close()
97
98	client := remote.NewClient(srv.URL, remote.WithToken("bad-token"))
99	ops := &dockerImageOps{
100		remoteClient: client,
101	}
102
103	_, err := ops.exchangeRegistryAuth(context.Background())
104
105	require.Error(t, err)
106	assert.Contains(t, err.Error(), "failed to verify token")
107}
108
109// TestExchangeRegistryAuth_TokenNotValid verifies that when VerifyAuth returns
110// Valid=false, exchangeRegistryAuth returns an error.
111func TestExchangeRegistryAuth_TokenNotValid(t *testing.T) {
112	srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
113		if r.URL.Path == "/admin/auth/verify" {
114			w.Header().Set("Content-Type", "application/json")
115			resp := dto.AuthVerifyResponse{Valid: false}
116			json.NewEncoder(w).Encode(resp) //nolint:errcheck
117			return
118		}
119		http.NotFound(w, r)
120	})
121	defer srv.Close()
122
123	client := remote.NewClient(srv.URL, remote.WithToken("expired-token"))
124	ops := &dockerImageOps{
125		remoteClient: client,
126	}
127
128	_, err := ops.exchangeRegistryAuth(context.Background())
129
130	require.Error(t, err)
131	assert.Contains(t, err.Error(), "token is not valid")
132}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close