internal/secretsengine: add unit tests

Cover which registries use the Docker Desktop session, keeping the production and staging sessions apart, caching the session until it expires, the retry interval after a failed lookup, and Disable.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
This commit is contained in:
Alano Terblanche
2026-10-22 11:21:56 +02:00
parent a46376f216
commit c4e317145e
@@ -0,0 +1,402 @@
//go:build go1.26
package secretsengine
import (
"context"
"errors"
"fmt"
"io/fs"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/docker/cli/cli/config/types"
seclient "github.com/docker/secrets-engine/client"
"github.com/docker/secrets-engine/client/dockerhub"
"gotest.tools/v3/assert"
)
var testNow = time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC)
type fakeHub struct {
session dockerhub.UserSession
sessionErr error
profile dockerhub.Profile
profileErr error
sessionCalls atomic.Int32
}
func (f *fakeHub) GetDefaultSession(context.Context) (dockerhub.UserSession, error) {
f.sessionCalls.Add(1)
return f.session, f.sessionErr
}
func (f *fakeHub) GetDefaultProfile(context.Context) (dockerhub.Profile, error) {
return f.profile, f.profileErr
}
type fakeClock struct {
mu sync.Mutex
t time.Time
}
func (c *fakeClock) now() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.t
}
func (c *fakeClock) advance(d time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.t = c.t.Add(d)
}
func hubSession(username string, expiresAt time.Time) dockerhub.UserSession {
s := dockerhub.UserSession{AccessToken: "desktop-oauth-token"}
s.Claims.Username = username
if !expiresAt.IsZero() {
s.Claims.ExpiresAt = &dockerhub.NumericDate{Time: expiresAt}
}
return s
}
func desktopCredentials() types.AuthConfig {
return types.AuthConfig{Username: "hubuser", Password: "desktop-oauth-token", ServerAddress: indexServer}
}
func stagingCredentials() types.AuthConfig {
return types.AuthConfig{Username: "stageuser", Password: "staging-oauth-token", ServerAddress: stagingRegistryHost}
}
type testProvider struct {
*provider
clock *fakeClock
connects atomic.Int32
staging *fakeHub
}
func newTestProvider(hub hubSessions, connectErr error) *testProvider {
tp := &testProvider{
clock: &fakeClock{t: testNow},
staging: &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile},
}
tp.provider = &provider{
hub: &session{
name: "Docker Hub",
connect: func() (hubSessions, error) {
tp.connects.Add(1)
if connectErr != nil {
return nil, connectErr
}
return hub, nil
},
timeout: time.Second,
now: tp.clock.now,
},
staging: &session{
name: "Docker Hub staging",
connect: func() (hubSessions, error) {
return tp.staging, nil
},
timeout: time.Second,
now: tp.clock.now,
},
}
return tp
}
func (tp *testProvider) signInToStaging() {
tp.staging.sessionErr = nil
tp.staging.session = hubSession("stageuser", testNow.Add(time.Hour))
tp.staging.session.AccessToken = "staging-oauth-token"
}
func connectError(reason seclient.ConnectReason) error {
return &seclient.ConnectError{
Reason: reason,
SocketPath: "/run/engine.sock",
Err: errors.New("dial unix /run/engine.sock"),
}
}
func TestDisableSkipsDockerDesktop(t *testing.T) {
Disable()
_, ok := AuthConfig("docker.io")
assert.Assert(t, !ok)
shared.hub.mu.Lock()
defer shared.hub.mu.Unlock()
assert.Assert(t, shared.hub.retryAt.IsZero(), "Docker Desktop must not be contacted once disabled")
assert.Assert(t, shared.hub.hub == nil)
}
func TestSharedProviderHasOneSessionPerEnvironment(t *testing.T) {
assert.Assert(t, shared.hub != nil)
assert.Assert(t, shared.staging != nil)
assert.Assert(t, shared.hub != shared.staging, "production and staging sessions must be separate")
}
func TestDockerHubUsesDesktopSession(t *testing.T) {
for _, host := range []string{"docker.io", "index.docker.io", "registry-1.docker.io", indexServer} {
t.Run(host, func(t *testing.T) {
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
p := newTestProvider(hub, nil)
got, ok := p.authConfig(host)
assert.Assert(t, ok)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, got.IdentityToken, "", "an IdentityToken is sent as a refresh token")
})
}
}
func TestOtherRegistriesDoNotContactDesktop(t *testing.T) {
hosts := []string{
"ghcr.io", "localhost:5000", "registry.example.com", "",
stagingRegistryHost + ".example.com", "example.com/" + stagingRegistryHost,
dhiRegistryHost + ".example.com", "example.com/" + dhiRegistryHost, "registry." + dhiRegistryHost,
"docker.io.example.com",
}
for _, host := range hosts {
t.Run(host, func(t *testing.T) {
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
p := newTestProvider(hub, nil)
p.signInToStaging()
_, ok := p.authConfig(host)
assert.Assert(t, !ok)
assert.Equal(t, p.connects.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
})
}
}
func TestDHIUsesDockerHubSession(t *testing.T) {
for _, host := range []string{dhiRegistryHost, "https://" + dhiRegistryHost, "https://" + dhiRegistryHost + "/"} {
t.Run(host, func(t *testing.T) {
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
p := newTestProvider(hub, nil)
p.signInToStaging()
want := desktopCredentials()
want.ServerAddress = dhiRegistryHost
got, ok := p.authConfig(host)
assert.Assert(t, ok)
assert.DeepEqual(t, got, want)
assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "the staging session must not be read for DHI")
got, ok = p.authConfig("docker.io")
assert.Assert(t, ok)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
})
}
}
func TestStagingRegistryUsesStagingSession(t *testing.T) {
for _, host := range []string{stagingRegistryHost, "https://" + stagingRegistryHost + "/"} {
t.Run(host, func(t *testing.T) {
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
p := newTestProvider(hub, nil)
p.signInToStaging()
got, ok := p.authConfig(host)
assert.Assert(t, ok)
assert.DeepEqual(t, got, stagingCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(0), "the production session must not be read for the staging registry")
})
}
}
func TestEnvironmentsDoNotMix(t *testing.T) {
t.Run("staging session is not used for docker.io", func(t *testing.T) {
p := newTestProvider(&fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}, nil)
p.signInToStaging()
_, ok := p.authConfig("docker.io")
assert.Assert(t, !ok)
got, ok := p.authConfig(stagingRegistryHost)
assert.Assert(t, ok)
assert.DeepEqual(t, got, stagingCredentials())
})
t.Run("production session is not used for the staging registry", func(t *testing.T) {
p := newTestProvider(&fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}, nil)
_, ok := p.authConfig(stagingRegistryHost)
assert.Assert(t, !ok)
got, ok := p.authConfig("docker.io")
assert.Assert(t, ok)
assert.DeepEqual(t, got, desktopCredentials())
})
}
func TestUsernameFromProfile(t *testing.T) {
hub := &fakeHub{
session: hubSession("", testNow.Add(time.Hour)),
profile: dockerhub.Profile{Username: "hubuser"},
}
p := newTestProvider(hub, nil)
got, ok := p.authConfig("docker.io")
assert.Assert(t, ok)
assert.DeepEqual(t, got, desktopCredentials())
}
func TestFallsBackWithoutUsableSession(t *testing.T) {
future := testNow.Add(time.Hour)
tests := []struct {
name string
hub *fakeHub
}{
{name: "secrets engine not running", hub: &fakeHub{sessionErr: connectError(seclient.ReasonNotRunning)}},
{name: "secrets engine socket not accessible", hub: &fakeHub{sessionErr: connectError(seclient.ReasonPermissionDenied)}},
{name: "nobody signed in", hub: &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}},
{name: "access denied", hub: &fakeHub{sessionErr: seclient.ErrAccessDenied}},
{name: "no username", hub: &fakeHub{session: hubSession("", future)}},
{name: "profile lookup fails", hub: &fakeHub{session: hubSession("", future), profileErr: errors.New("boom")}},
{name: "expired", hub: &fakeHub{session: hubSession("hubuser", testNow.Add(-time.Minute))}},
{name: "expires within the skew", hub: &fakeHub{session: hubSession("hubuser", testNow.Add(sessionExpirySkew/2))}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := newTestProvider(tc.hub, nil)
for range 2 {
_, ok := p.authConfig("docker.io")
assert.Assert(t, !ok)
}
assert.Equal(t, tc.hub.sessionCalls.Load(), int32(1), "Desktop must not be asked again within retryInterval")
})
}
}
func TestFallsBackWhenClientCannotBeCreated(t *testing.T) {
p := newTestProvider(nil, errors.New("no socket path"))
for range 2 {
_, ok := p.authConfig("docker.io")
assert.Assert(t, !ok)
}
assert.Equal(t, p.connects.Load(), int32(1))
}
func TestRetriesAfterInterval(t *testing.T) {
hub := &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}
p := newTestProvider(hub, nil)
_, ok := p.authConfig("docker.io")
assert.Assert(t, !ok)
hub.sessionErr = nil
hub.session = hubSession("hubuser", testNow.Add(time.Hour))
p.clock.advance(retryInterval - time.Second)
_, ok = p.authConfig("docker.io")
assert.Assert(t, !ok)
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
p.clock.advance(time.Second)
got, ok := p.authConfig("docker.io")
assert.Assert(t, ok)
assert.DeepEqual(t, got, desktopCredentials())
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
}
func TestReusesSessionUntilExpiry(t *testing.T) {
expiresAt := testNow.Add(10 * time.Minute)
hub := &fakeHub{session: hubSession("hubuser", expiresAt)}
p := newTestProvider(hub, nil)
_, ok := p.authConfig("docker.io")
assert.Assert(t, ok)
p.clock.advance(5 * time.Minute)
_, ok = p.authConfig("docker.io")
assert.Assert(t, ok)
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
hub.session = hubSession("hubuser", expiresAt.Add(time.Hour))
hub.session.AccessToken = "refreshed-token"
p.clock.advance(5*time.Minute - sessionExpirySkew)
got, ok := p.authConfig("docker.io")
assert.Assert(t, ok)
assert.Equal(t, got.Password, "refreshed-token")
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
}
func TestRefetchesSessionWithoutExpiry(t *testing.T) {
hub := &fakeHub{session: hubSession("hubuser", time.Time{})}
p := newTestProvider(hub, nil)
_, ok := p.authConfig("docker.io")
assert.Assert(t, ok)
p.clock.advance(sessionTTL - time.Second)
_, ok = p.authConfig("docker.io")
assert.Assert(t, ok)
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
p.clock.advance(time.Second)
_, ok = p.authConfig("docker.io")
assert.Assert(t, ok)
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
}
func TestConcurrentLookupsShareOneFetch(t *testing.T) {
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
p := newTestProvider(hub, nil)
const lookups = 20
results := make([]types.AuthConfig, lookups)
var wg sync.WaitGroup
for i := range lookups {
wg.Go(func() {
auth, ok := p.authConfig("docker.io")
assert.Check(t, ok)
results[i] = auth
})
}
wg.Wait()
for _, auth := range results {
assert.DeepEqual(t, auth, desktopCredentials())
}
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
assert.Equal(t, p.connects.Load(), int32(1))
}
func TestConnectSkipsMissingSocket(t *testing.T) {
_, err := connect(filepath.Join(t.TempDir(), "engine.sock"))
assert.Assert(t, errors.Is(err, fs.ErrNotExist), err)
assert.Assert(t, isSessionUnavailable(err))
}
func TestIsSessionUnavailable(t *testing.T) {
for _, err := range []error{
connectError(seclient.ReasonNotRunning),
fmt.Errorf("get session: %w", connectError(seclient.ReasonNotRunning)),
connectError(seclient.ReasonPermissionDenied),
connectError(seclient.ReasonTimeout),
connectError(seclient.ReasonUnknown),
dockerhub.ErrNoSession,
dockerhub.ErrNoDefaultProfile,
errSessionExpired,
fs.ErrNotExist,
} {
assert.Assert(t, isSessionUnavailable(err), err)
}
for _, err := range []error{
seclient.ErrAccessDenied,
errNoUsername,
context.DeadlineExceeded,
} {
assert.Assert(t, !isSessionUnavailable(err), err)
}
}