Files
dpsapi/helpers_test.go
T
bryanpedini 88d7d6d2b9 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.
2026-10-06 10:52:25 +02:00

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
}
}