GET /api/v1/containers behind bearer token auth, JSON errors for 401/404/405/500, one audit row per authenticated request, graceful shutdown; unit and e2e tests for auth, routing, auditing, request logging and server lifecycle.
150 lines
5.0 KiB
Go
150 lines
5.0 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
||
// Copyright (C) 2026 Bryan Joshua Pedini
|
||
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
)
|
||
|
||
// fakeLookup is an in-memory TokenLookup
|
||
type fakeLookup struct {
|
||
mu sync.Mutex
|
||
tokens map[string]Token
|
||
err error
|
||
calls []string
|
||
}
|
||
|
||
func (f *fakeLookup) GetToken(ctx context.Context, value string) (Token, error) {
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
f.calls = append(f.calls, value)
|
||
if f.err != nil {
|
||
return Token{}, f.err
|
||
}
|
||
tok, ok := f.tokens[value]
|
||
if !ok {
|
||
return Token{}, ErrTokenNotFound
|
||
}
|
||
return tok, nil
|
||
}
|
||
|
||
func newFakeLookup() *fakeLookup {
|
||
return &fakeLookup{tokens: map[string]Token{
|
||
"good": {Token: "good", Active: true},
|
||
"Good": {Token: "Good", Active: true},
|
||
"inactive": {Token: "inactive", Active: false},
|
||
}}
|
||
}
|
||
|
||
func headerWith(values ...string) http.Header {
|
||
h := http.Header{}
|
||
for _, v := range values {
|
||
h.Add("Authorization", v)
|
||
}
|
||
return h
|
||
}
|
||
|
||
func TestParseAuthorization(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
name string
|
||
header http.Header
|
||
token string
|
||
class string
|
||
}{
|
||
{"missing", http.Header{}, "", authMissingHeader},
|
||
{"other headers only", http.Header{"X-Authorization": {"Bearer good"}}, "", authMissingHeader},
|
||
{"empty", headerWith(""), "", authEmptyHeader},
|
||
{"multiple", headerWith("Bearer good", "Bearer good"), "", authMultipleHeaders},
|
||
{"multiple with empty", headerWith("", "Bearer good"), "", authMultipleHeaders},
|
||
{"basic", headerWith("Basic Z29vZDo="), "", authBadScheme},
|
||
{"token scheme", headerWith("Token good"), "", authBadScheme},
|
||
{"no space", headerWith("Bearergood"), "", authBadScheme},
|
||
{"tab separator", headerWith("Bearer\tgood"), "", authBadScheme},
|
||
{"leading space", headerWith(" Bearer good"), "", authBadScheme},
|
||
{"bare token", headerWith("good"), "", authBadScheme},
|
||
{"unicode scheme", headerWith("Beаrer good"), "", authBadScheme}, // Cyrillic a
|
||
{"scheme only", headerWith("Bearer"), "", authEmptyToken},
|
||
{"scheme and space", headerWith("Bearer "), "", authEmptyToken},
|
||
{"ok", headerWith("Bearer good"), "good", ""},
|
||
{"lowercase scheme", headerWith("bearer good"), "good", ""},
|
||
{"uppercase scheme", headerWith("BEARER good"), "good", ""},
|
||
{"double space kept", headerWith("Bearer good"), " good", ""},
|
||
{"trailing kept", headerWith("Bearer good "), "good ", ""},
|
||
{"extra words kept", headerWith("Bearer good extra"), "good extra", ""},
|
||
{"comma kept", headerWith("Bearer good, Bearer other"), "good, Bearer other", ""},
|
||
}
|
||
for _, c := range cases {
|
||
token, class := parseAuthorization(c.header)
|
||
if token != c.token || class != c.class {
|
||
t.Errorf("%s: got (%q, %q), want (%q, %q)", c.name, token, class, c.token, c.class)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestAuthenticate(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
name string
|
||
header http.Header
|
||
class string
|
||
token string
|
||
}{
|
||
{"missing", http.Header{}, authMissingHeader, ""},
|
||
{"empty", headerWith(""), authEmptyHeader, ""},
|
||
{"wrong scheme", headerWith("Basic good"), authBadScheme, ""},
|
||
{"empty bearer", headerWith("Bearer "), authEmptyToken, ""},
|
||
{"malformed bearer", headerWith("Bearer good"), authUnknownToken, ""},
|
||
{"multiple", headerWith("Bearer good", "Bearer good"), authMultipleHeaders, ""},
|
||
{"unknown", headerWith("Bearer nope"), authUnknownToken, ""},
|
||
{"case changed", headerWith("Bearer GOOD"), authUnknownToken, ""},
|
||
{"near match", headerWith("Bearer goo"), authUnknownToken, ""},
|
||
{"inactive", headerWith("Bearer inactive"), authInactiveToken, ""},
|
||
{"valid", headerWith("Bearer good"), authOK, "good"},
|
||
{"valid case-distinct token", headerWith("bearer Good"), authOK, "Good"},
|
||
}
|
||
lookup := newFakeLookup()
|
||
for _, c := range cases {
|
||
tok, class, err := authenticate(context.Background(), traceDiscard(), lookup, c.header)
|
||
if err != nil || class != c.class || tok.Token != c.token {
|
||
t.Errorf("%s: got (%q, %q, %v), want (%q, %q)", c.name, tok.Token, class, err, c.token, c.class)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestAuthenticateLookupError(t *testing.T) {
|
||
t.Parallel()
|
||
lookup := &fakeLookup{err: errors.New("database is locked")}
|
||
_, class, err := authenticate(context.Background(), traceDiscard(), lookup, headerWith("Bearer good"))
|
||
if err == nil || class != authLookupError {
|
||
t.Fatalf("got (%q, %v)", class, err)
|
||
}
|
||
}
|
||
|
||
func TestAuthenticateDoesNotLookUpMalformedHeaders(t *testing.T) {
|
||
t.Parallel()
|
||
lookup := newFakeLookup()
|
||
for _, h := range []http.Header{{}, headerWith(""), headerWith("Basic x"), headerWith("Bearer "), headerWith("Bearer a", "Bearer b")} {
|
||
authenticate(context.Background(), traceDiscard(), lookup, h)
|
||
}
|
||
if len(lookup.calls) != 0 {
|
||
t.Fatalf("lookups performed: %q", lookup.calls)
|
||
}
|
||
}
|
||
|
||
func TestAuthenticationClassesAreFixedStrings(t *testing.T) {
|
||
t.Parallel()
|
||
for _, c := range []string{authMissingHeader, authEmptyHeader, authMultipleHeaders, authBadScheme,
|
||
authEmptyToken, authUnknownToken, authInactiveToken, authLookupError, authOK} {
|
||
if c == "" || strings.ContainsAny(c, " =\"") {
|
||
t.Errorf("class %q is not a plain identifier", c)
|
||
}
|
||
}
|
||
}
|