174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
//go:build unix
|
|
|
|
package authn
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring"
|
|
fakekeyring "gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring/fake"
|
|
)
|
|
|
|
func TestNewTokenSource_RequiresCallback(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("expected panic on nil onToken")
|
|
}
|
|
}()
|
|
NewTokenSource(fakekeyring.New(), "gitea",
|
|
Config{Issuer: "https://x", ClientID: "y"},
|
|
EnsureOptions{LockPath: t.TempDir() + "/lock"}, nil)
|
|
}
|
|
|
|
func TestNewTokenSource_RequiresLockPath(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("expected panic on empty LockPath")
|
|
}
|
|
}()
|
|
NewTokenSource(fakekeyring.New(), "gitea",
|
|
Config{Issuer: "https://x", ClientID: "y"},
|
|
EnsureOptions{}, func(keyring.TokenSet) {})
|
|
}
|
|
|
|
func TestTokenSource_Start_DeliversInitialToken(t *testing.T) {
|
|
store := fakekeyring.New()
|
|
_ = store.Set("gitea", keyring.TokenSet{
|
|
IDToken: "fresh",
|
|
AccessToken: "at-0",
|
|
IDExpiresAt: time.Now().Add(time.Hour),
|
|
Issuer: "x",
|
|
ClientID: "y",
|
|
})
|
|
|
|
var got []string
|
|
var mu sync.Mutex
|
|
cb := func(ts keyring.TokenSet) {
|
|
mu.Lock()
|
|
got = append(got, ts.AccessToken)
|
|
mu.Unlock()
|
|
}
|
|
|
|
src := NewTokenSource(store, "gitea",
|
|
Config{Issuer: "https://example.test", ClientID: "y"},
|
|
EnsureOptions{LockPath: t.TempDir() + "/lock"}, cb)
|
|
|
|
ts, err := src.Start(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if ts.AccessToken != "at-0" {
|
|
t.Fatalf("Start returned %q, want at-0", ts.AccessToken)
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(got) != 1 || got[0] != "at-0" {
|
|
t.Fatalf("callback got %v, want [at-0]", got)
|
|
}
|
|
}
|
|
|
|
func TestTokenSource_Run_RenewsAndNotifies(t *testing.T) {
|
|
stub := newStubAuthentik(t)
|
|
store := fakekeyring.New()
|
|
// Seed a STALE token (already expired) with a refresh token so the
|
|
// renewal path fires immediately.
|
|
_ = store.Set("gitea", keyring.TokenSet{
|
|
IDToken: "old",
|
|
AccessToken: "at-stale",
|
|
RefreshToken: "rt-1",
|
|
IDExpiresAt: time.Now().Add(-time.Minute),
|
|
Issuer: stub.srv.URL,
|
|
ClientID: "test-client",
|
|
Scopes: []string{"openid"},
|
|
})
|
|
|
|
tokens := make(chan string, 4)
|
|
src := NewTokenSource(store, "gitea",
|
|
Config{Issuer: stub.srv.URL, ClientID: "test-client"},
|
|
EnsureOptions{LockPath: t.TempDir() + "/lock"},
|
|
func(ts keyring.TokenSet) { tokens <- ts.AccessToken })
|
|
src.minWait = time.Millisecond // refresh almost immediately
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
done := make(chan struct{})
|
|
go func() { _ = src.Run(ctx); close(done) }()
|
|
|
|
select {
|
|
case got := <-tokens:
|
|
if got != "at-refreshed" {
|
|
t.Fatalf("renewed token = %q, want at-refreshed", got)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for renewal callback")
|
|
}
|
|
|
|
// The rotated token must be persisted to the wallet too.
|
|
stored, err := store.Get("gitea")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if stored.AccessToken != "at-refreshed" {
|
|
t.Fatalf("wallet access token = %q, want at-refreshed", stored.AccessToken)
|
|
}
|
|
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Run did not exit after cancel")
|
|
}
|
|
}
|
|
|
|
func TestTokenSource_Run_StopsOnCancel(t *testing.T) {
|
|
store := fakekeyring.New()
|
|
_ = store.Set("gitea", keyring.TokenSet{
|
|
IDToken: "fresh",
|
|
AccessToken: "at-0",
|
|
IDExpiresAt: time.Now().Add(time.Hour),
|
|
Issuer: "x",
|
|
ClientID: "y",
|
|
})
|
|
src := NewTokenSource(store, "gitea",
|
|
Config{Issuer: "https://example.test", ClientID: "y"},
|
|
EnsureOptions{LockPath: t.TempDir() + "/lock"},
|
|
func(keyring.TokenSet) {})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() { done <- src.Run(ctx) }()
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if err != context.Canceled {
|
|
t.Fatalf("Run returned %v, want context.Canceled", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Run did not return after cancel")
|
|
}
|
|
}
|
|
|
|
func TestTokenHolder(t *testing.T) {
|
|
h := NewTokenHolder()
|
|
if h.AccessToken() != "" {
|
|
t.Fatal("empty holder should return empty access token")
|
|
}
|
|
if _, err := h.Bearer(); err == nil {
|
|
t.Fatal("empty holder Bearer should error")
|
|
}
|
|
h.Set(keyring.TokenSet{AccessToken: "at-1"})
|
|
if h.AccessToken() != "at-1" {
|
|
t.Fatalf("AccessToken = %q, want at-1", h.AccessToken())
|
|
}
|
|
if b, err := h.Bearer(); err != nil || b != "Bearer at-1" {
|
|
t.Fatalf("Bearer = %q, %v", b, err)
|
|
}
|
|
h.Set(keyring.TokenSet{AccessToken: "at-2"})
|
|
if h.AccessToken() != "at-2" {
|
|
t.Fatalf("AccessToken = %q, want at-2 after update", h.AccessToken())
|
|
}
|
|
}
|