feat: The Other Dude v9.0.1 — full-featured email system

ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-08 17:46:37 -05:00
commit b840047e19
511 changed files with 106948 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
// Package testutil provides shared testcontainer helpers for integration tests.
//
// All helpers start real infrastructure containers (PostgreSQL, Redis, NATS) via
// testcontainers-go and return connection strings plus cleanup functions. Tests
// using these helpers require a running Docker daemon and are skipped automatically
// when `go test -short` is used.
package testutil
import (
"context"
"fmt"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/testcontainers/testcontainers-go"
tcnats "github.com/testcontainers/testcontainers-go/modules/nats"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/modules/redis"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/mikrotik-portal/poller/internal/store"
)
// devicesSchema is the minimal DDL needed for integration tests against the
// devices table. It mirrors the production schema but omits RLS policies and
// other tables the poller doesn't read.
const devicesSchema = `
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE TABLE IF NOT EXISTS devices (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
tenant_id UUID NOT NULL,
hostname VARCHAR(255) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
api_port INTEGER NOT NULL DEFAULT 8728,
api_ssl_port INTEGER NOT NULL DEFAULT 8729,
model VARCHAR(255),
serial_number VARCHAR(255),
firmware_version VARCHAR(100),
routeros_version VARCHAR(100),
routeros_major_version INTEGER,
uptime_seconds INTEGER,
last_seen TIMESTAMPTZ,
encrypted_credentials BYTEA,
status VARCHAR(20) NOT NULL DEFAULT 'unknown',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
`
// SetupPostgres starts a PostgreSQL container using the TimescaleDB image and
// applies the devices table schema. Returns the connection string and a cleanup
// function that terminates the container.
func SetupPostgres(t *testing.T) (connStr string, cleanup func()) {
t.Helper()
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx := context.Background()
pgContainer, err := postgres.Run(ctx,
"postgres:17-alpine",
postgres.WithDatabase("mikrotik_test"),
postgres.WithUsername("postgres"),
postgres.WithPassword("test"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(60*time.Second),
),
)
if err != nil {
t.Fatalf("starting PostgreSQL container: %v", err)
}
connStr, err = pgContainer.ConnectionString(ctx, "sslmode=disable")
if err != nil {
_ = pgContainer.Terminate(ctx)
t.Fatalf("getting PostgreSQL connection string: %v", err)
}
// Apply schema using pgx directly.
conn, err := pgx.Connect(ctx, connStr)
if err != nil {
_ = pgContainer.Terminate(ctx)
t.Fatalf("connecting to PostgreSQL to apply schema: %v", err)
}
defer conn.Close(ctx)
if _, err := conn.Exec(ctx, devicesSchema); err != nil {
_ = pgContainer.Terminate(ctx)
t.Fatalf("applying devices schema: %v", err)
}
cleanup = func() {
if err := pgContainer.Terminate(ctx); err != nil {
t.Logf("warning: terminating PostgreSQL container: %v", err)
}
}
return connStr, cleanup
}
// SetupRedis starts a Redis container and returns the address (host:port) plus
// a cleanup function.
func SetupRedis(t *testing.T) (addr string, cleanup func()) {
t.Helper()
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx := context.Background()
redisContainer, err := redis.Run(ctx,
"redis:7-alpine",
testcontainers.WithWaitStrategy(
wait.ForLog("Ready to accept connections").
WithStartupTimeout(30*time.Second),
),
)
if err != nil {
t.Fatalf("starting Redis container: %v", err)
}
host, err := redisContainer.Host(ctx)
if err != nil {
_ = redisContainer.Terminate(ctx)
t.Fatalf("getting Redis host: %v", err)
}
port, err := redisContainer.MappedPort(ctx, "6379")
if err != nil {
_ = redisContainer.Terminate(ctx)
t.Fatalf("getting Redis mapped port: %v", err)
}
addr = fmt.Sprintf("%s:%s", host, port.Port())
cleanup = func() {
if err := redisContainer.Terminate(ctx); err != nil {
t.Logf("warning: terminating Redis container: %v", err)
}
}
return addr, cleanup
}
// SetupNATS starts a NATS container with JetStream enabled and returns the NATS
// URL (nats://host:port) plus a cleanup function.
func SetupNATS(t *testing.T) (url string, cleanup func()) {
t.Helper()
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx := context.Background()
natsContainer, err := tcnats.Run(ctx,
"nats:2-alpine",
testcontainers.WithCmd("--jetstream"),
testcontainers.WithWaitStrategy(
wait.ForLog("Server is ready").
WithStartupTimeout(30*time.Second),
),
)
if err != nil {
t.Fatalf("starting NATS container: %v", err)
}
host, err := natsContainer.Host(ctx)
if err != nil {
_ = natsContainer.Terminate(ctx)
t.Fatalf("getting NATS host: %v", err)
}
port, err := natsContainer.MappedPort(ctx, "4222")
if err != nil {
_ = natsContainer.Terminate(ctx)
t.Fatalf("getting NATS mapped port: %v", err)
}
url = fmt.Sprintf("nats://%s:%s", host, port.Port())
cleanup = func() {
if err := natsContainer.Terminate(ctx); err != nil {
t.Logf("warning: terminating NATS container: %v", err)
}
}
return url, cleanup
}
// InsertTestDevice inserts a device row into the database and returns the
// generated UUID. The caller provides a store.Device with fields to populate;
// fields left at zero values use column defaults.
func InsertTestDevice(t *testing.T, connStr string, dev store.Device) string {
t.Helper()
ctx := context.Background()
conn, err := pgx.Connect(ctx, connStr)
if err != nil {
t.Fatalf("connecting to PostgreSQL for InsertTestDevice: %v", err)
}
defer conn.Close(ctx)
var id string
err = conn.QueryRow(ctx,
`INSERT INTO devices (tenant_id, hostname, ip_address, api_port, api_ssl_port,
encrypted_credentials, routeros_version, routeros_major_version)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id::text`,
dev.TenantID,
coalesce(dev.IPAddress, "test-device"), // hostname defaults to ip if not set
dev.IPAddress,
coalesceInt(dev.APIPort, 8728),
coalesceInt(dev.APISSLPort, 8729),
dev.EncryptedCredentials,
dev.RouterOSVersion,
dev.MajorVersion,
).Scan(&id)
if err != nil {
t.Fatalf("inserting test device: %v", err)
}
return id
}
func coalesce(s, fallback string) string {
if s == "" {
return fallback
}
return s
}
func coalesceInt(v, fallback int) int {
if v == 0 {
return fallback
}
return v
}