Files
dpsapi/auth_test.go
T
bryanpedini b792ad256e feat(api): add authenticated containers http api
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.
2026-10-06 10:52:25 +02:00

150 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}
}
}