feat(core): add configuration, logging and sqlite persistence
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.
This commit is contained in:
1 parent
fe0cd4bf6d
commit
88d7d6d2b9
11 files changed
+2426
No files matched your search
+182
@@ -0,0 +1,182 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user