DB_PATH, HTTP_ADDR, HTTP_PORT and LOG_LEVEL parsing with validation; slog text logger with a TRACE level and token redaction; embedded idempotent schema on CGO-free SQLite (WAL, busy timeout); token store (create, list, deactivate, authenticate) and append-only audit store with ordered, limited and windowed queries; shared unit test helpers.
183 lines
3.9 KiB
Go
183 lines
3.9 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/moby/moby/api/types/container"
|
|
)
|
|
|
|
// syncBuffer is a goroutine-safe bytes.Buffer for capturing logs and output
|
|
type syncBuffer struct {
|
|
mu sync.Mutex
|
|
b bytes.Buffer
|
|
}
|
|
|
|
func (s *syncBuffer) Write(p []byte) (int, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.b.Write(p)
|
|
}
|
|
|
|
func (s *syncBuffer) String() string {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.b.String()
|
|
}
|
|
|
|
// newTestLogger returns a logger writing to a captured buffer
|
|
func newTestLogger(level slog.Level) (*slog.Logger, *syncBuffer) {
|
|
buf := &syncBuffer{}
|
|
return NewLogger(buf, level), buf
|
|
}
|
|
|
|
// traceDiscard is a TRACE-level logger whose output is dropped (exercises every log call)
|
|
func traceDiscard() *slog.Logger {
|
|
return NewLogger(io.Discard, LevelTrace)
|
|
}
|
|
|
|
// fakeClock returns a fixed time advancing by step on every call
|
|
type fakeClock struct {
|
|
mu sync.Mutex
|
|
t time.Time
|
|
step time.Duration
|
|
}
|
|
|
|
func newFakeClock(start time.Time, step time.Duration) *fakeClock {
|
|
return &fakeClock{t: start, step: step}
|
|
}
|
|
|
|
func (c *fakeClock) Now() time.Time {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
t := c.t
|
|
c.t = c.t.Add(c.step)
|
|
return t
|
|
}
|
|
|
|
// seqGenerator returns the given tokens in order, then an error
|
|
func seqGenerator(values ...string) func() (string, error) {
|
|
var mu sync.Mutex
|
|
i := 0
|
|
return func() (string, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if i >= len(values) {
|
|
return "", errors.New("generator exhausted")
|
|
}
|
|
v := values[i]
|
|
i++
|
|
return v, nil
|
|
}
|
|
}
|
|
|
|
// testDBPath returns a fresh database path inside the test's temporary directory
|
|
func testDBPath(t *testing.T) string {
|
|
t.Helper()
|
|
return filepath.Join(t.TempDir(), "dpsapi.db")
|
|
}
|
|
|
|
// openTestDB opens and initializes a fresh database
|
|
func openTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := OpenDatabase(context.Background(), testDBPath(t), traceDiscard())
|
|
if err != nil {
|
|
t.Fatalf("OpenDatabase: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
return db
|
|
}
|
|
|
|
// mustExec executes a statement or fails the test
|
|
func mustExec(t *testing.T, db *sql.DB, query string, args ...any) {
|
|
t.Helper()
|
|
if _, err := db.Exec(query, args...); err != nil {
|
|
t.Fatalf("exec %q: %v", query, err)
|
|
}
|
|
}
|
|
|
|
// countRows returns the number of rows in a table
|
|
func countRows(t *testing.T, db *sql.DB, table string) int {
|
|
t.Helper()
|
|
var n int
|
|
if err := db.QueryRow(`SELECT COUNT(*) FROM ` + table).Scan(&n); err != nil {
|
|
t.Fatalf("count %s: %v", table, err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
// fakeDocker is an in-memory ContainerLister
|
|
type fakeDocker struct {
|
|
mu sync.Mutex
|
|
containers []container.Summary
|
|
err error
|
|
calls int
|
|
closed bool
|
|
onList func(ctx context.Context)
|
|
}
|
|
|
|
func (f *fakeDocker) ListRunningContainers(ctx context.Context) ([]container.Summary, error) {
|
|
f.mu.Lock()
|
|
f.calls++
|
|
hook := f.onList
|
|
f.mu.Unlock()
|
|
if hook != nil {
|
|
hook(ctx)
|
|
}
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
return f.containers, nil
|
|
}
|
|
|
|
func (f *fakeDocker) Close() error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.closed = true
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeDocker) Calls() int {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.calls
|
|
}
|
|
|
|
// makeContainers builds n distinct container summaries
|
|
func makeContainers(n int) []container.Summary {
|
|
out := make([]container.Summary, 0, n)
|
|
for i := 0; i < n; i++ {
|
|
out = append(out, container.Summary{
|
|
ID: fmt.Sprintf("%064x", i+1),
|
|
Names: []string{fmt.Sprintf("/c%d", i)},
|
|
Image: "busybox:latest",
|
|
State: container.StateRunning,
|
|
Status: "Up 1 second",
|
|
Labels: map[string]string{"index": fmt.Sprint(i)},
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// envMap returns a lookupEnv function backed by a map
|
|
func envMap(m map[string]string) func(string) (string, bool) {
|
|
return func(k string) (string, bool) {
|
|
v, ok := m[k]
|
|
return v, ok
|
|
}
|
|
}
|