package authn
import (
"bytes"
"context"
"database/sql"
"errors"
"net/http"
"net/http/httptest"
"testing"
)
func TestHashToken_IsSHA256AndStable(t *testing.T) {
a := HashToken("s3cret")
b := HashToken("s3cret")
if len(a) != 32 {
t.Fatalf("hash length = %d, want 32", len(a))
}
if !bytes.Equal(a, b) {
t.Fatal("hashing the same token twice produced different values")
}
if bytes.Equal(a, HashToken("s3cres")) {
t.Fatal("distinct tokens hashed to the same value")
}
}
func TestBearerFromRequest(t *testing.T) {
cases := map[string]struct{ header, want string }{
"bearer": {"Bearer abc123", "abc123"},
"lowercase scheme": {"bearer abc123", "abc123"},
"mixed case scheme": {"BeArEr abc123", "abc123"},
"padded": {"Bearer abc123 ", "abc123"},
"basic is not ours": {"Basic dXNlcjpwYXNz", ""},
"no scheme": {"abc123", ""},
"absent": {"", ""},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
if c.header != "" {
r.Header.Set("Authorization", c.header)
}
if got := BearerFromRequest(r); got != c.want {
t.Fatalf("BearerFromRequest = %q, want %q", got, c.want)
}
})
}
}
func TestResolveAgentToken_Accepted(t *testing.T) {
store := newStubStore()
want := store.add("live-token", "laptop")
got, err := ResolveAgentToken(context.Background(), store, "live-token")
if err != nil {
t.Fatalf("ResolveAgentToken: %v", err)
}
if got.ID != want.ID || got.Name != "laptop" {
t.Fatalf("resolved %+v, want id %d name %q", got, want.ID, "laptop")
}
if got.IsRevoked() {
t.Fatal("live token reported as revoked")
}
}
func TestResolveAgentToken_Revoked(t *testing.T) {
store := newStubStore()
store.revoke("dead-token", "cron")
_, err := ResolveAgentToken(context.Background(), store, "dead-token")
if !errors.Is(err, ErrRevokedToken) {
t.Fatalf("error = %v, want ErrRevokedToken", err)
}
// A revoked token must not be reported as unknown: the operator needs to
// tell "I killed this" from "this never existed".
if errors.Is(err, ErrUnknownToken) {
t.Fatalf("revoked token also reported as unknown: %v", err)
}
if !IsAuthFailure(err) {
t.Fatalf("revocation must be a permanent auth failure: %v", err)
}
}
func TestResolveAgentToken_Unknown(t *testing.T) {
store := newStubStore()
store.add("live-token", "laptop")
_, err := ResolveAgentToken(context.Background(), store, "never-issued")
if !errors.Is(err, ErrUnknownToken) {
t.Fatalf("error = %v, want ErrUnknownToken", err)
}
if !IsAuthFailure(err) {
t.Fatalf("unknown token must be a permanent auth failure: %v", err)
}
}
// db/ may hand back a bare sql.ErrNoRows from QueryRow().Scan(); it means the
// same thing as ErrUnknownToken and must not be mistaken for a store outage.
func TestResolveAgentToken_SQLNoRowsIsUnknown(t *testing.T) {
store := newStubStore()
store.err = sql.ErrNoRows
_, err := ResolveAgentToken(context.Background(), store, "whatever")
if !errors.Is(err, ErrUnknownToken) {
t.Fatalf("error = %v, want ErrUnknownToken", err)
}
}
func TestResolveAgentToken_EmptyIsNoToken(t *testing.T) {
store := newStubStore()
_, err := ResolveAgentToken(context.Background(), store, "")
if !errors.Is(err, ErrNoToken) {
t.Fatalf("error = %v, want ErrNoToken", err)
}
if store.calls != 0 {
t.Fatalf("store consulted %d times for an absent token, want 0", store.calls)
}
}
// A store outage must never read as a bad credential: fail closed, but tell the
// caller it is transient so it answers 503 and the agent retries.
func TestResolveAgentToken_StoreFailureIsTransient(t *testing.T) {
boom := errors.New("connection refused")
store := newStubStore()
store.add("live-token", "laptop")
store.err = boom
_, err := ResolveAgentToken(context.Background(), store, "live-token")
if !errors.Is(err, boom) {
t.Fatalf("error = %v, want it to wrap the store error", err)
}
if IsAuthFailure(err) {
t.Fatalf("store failure must not be a permanent auth failure: %v", err)
}
}
// The constant-time re-check exists so that a store which matched loosely — by
// prefix, or on the wrong column — cannot authenticate anybody.
func TestResolveAgentToken_HashMismatchRejected(t *testing.T) {
store := newStubStore()
store.put(HashToken("presented"), AgentToken{
ID: 7,
Name: "sloppy-store",
Hash: HashToken("something-else"),
})
_, err := ResolveAgentToken(context.Background(), store, "presented")
if !errors.Is(err, ErrInvalidToken) {
t.Fatalf("error = %v, want ErrInvalidToken", err)
}
}
func TestResolveAgentToken_NilStoreIsNotAnAuthFailure(t *testing.T) {
_, err := ResolveAgentToken(context.Background(), nil, "live-token")
if err == nil {
t.Fatal("nil store must be an error")
}
if IsAuthFailure(err) {
t.Fatalf("a wiring bug must not read as a bad credential: %v", err)
}
}