Files
DDI/server/cmd/ddi-server/main_test.go
T
sir_melonchenandClaude Opus 5.5 df25705b6e feat(server): add license set-key command reading the key from stdin
Validates and activates the key like PUT /api/v1/license (same service
call, audited as the system actor) and prints the license state without
the key. Exit codes: 2 bad input, 3 key rejected, 4 license server
unreachable or untrusted (nothing stored).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:28:33 +02:00

220 lines
6.8 KiB
Go

// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
package main
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/jackc/pgx/v5"
)
func lookupFrom(m map[string]string) func(string) (string, bool) {
return func(k string) (string, bool) { v, ok := m[k]; return v, ok }
}
func TestRunVersionAndHelp(t *testing.T) {
var out, errOut bytes.Buffer
if code := run(context.Background(), []string{"version"}, lookupFrom(nil), nil, &out, &errOut); code != 0 {
t.Fatalf("version exit code %d", code)
}
if !strings.HasPrefix(out.String(), "ddi-server ") {
t.Errorf("version output %q", out.String())
}
out.Reset()
if code := run(context.Background(), []string{"help"}, lookupFrom(nil), nil, &out, &errOut); code != 0 || !strings.Contains(out.String(), "bootstrap-admin") {
t.Errorf("help: code %d, output %q", code, out.String())
}
}
func TestRunUnknownCommand(t *testing.T) {
var out, errOut bytes.Buffer
if code := run(context.Background(), []string{"frobnicate"}, lookupFrom(nil), nil, &out, &errOut); code != 2 {
t.Errorf("exit code %d, want 2", code)
}
}
func TestRunInvalidConfig(t *testing.T) {
var errOut bytes.Buffer
code := run(context.Background(), []string{"serve"}, lookupFrom(map[string]string{"DDI_HTTP_LISTEN": "bad"}), nil, io.Discard, &errOut)
if code != 1 || !strings.Contains(errOut.String(), "invalid configuration") {
t.Errorf("code %d, stderr %q", code, errOut.String())
}
}
func TestServeFailsWithoutMasterKey(t *testing.T) {
var errOut bytes.Buffer
env := map[string]string{"DDI_MASTER_KEY_FILE": filepath.Join(t.TempDir(), "missing.key"), "DDI_DATABASE_URL": "postgres://x"}
if code := run(context.Background(), []string{"serve"}, lookupFrom(env), nil, io.Discard, &errOut); code != 1 || !strings.Contains(errOut.String(), "master key") {
t.Errorf("code %d, stderr %q", code, errOut.String())
}
}
func TestGenMasterKey(t *testing.T) {
path := filepath.Join(t.TempDir(), "sub", "master.key")
var out, errOut bytes.Buffer
if code := run(context.Background(), []string{"gen-master-key", "-out", path}, lookupFrom(nil), nil, &out, &errOut); code != 0 {
t.Fatalf("code %d: %s", code, errOut.String())
}
data, err := os.ReadFile(path)
if err != nil || !strings.Contains(string(data), "k1 ") {
t.Fatalf("key file: %q %v", data, err)
}
if code := run(context.Background(), []string{"gen-master-key", "-out", path}, lookupFrom(nil), nil, &out, &errOut); code != 1 {
t.Error("existing key file must not be overwritten")
}
}
// testDatabase creates a private schema and returns a URL that uses it.
func testDatabase(t *testing.T) string {
t.Helper()
base := os.Getenv("DDI_TEST_DATABASE_URL")
if base == "" {
if os.Getenv("DDI_REQUIRE_DB_TESTS") == "1" {
t.Fatal("DDI_TEST_DATABASE_URL is not set but DDI_REQUIRE_DB_TESTS=1")
}
t.Skip("DDI_TEST_DATABASE_URL not set")
}
var b [6]byte
_, _ = rand.Read(b[:])
schema := "t_" + hex.EncodeToString(b[:])
ctx := context.Background()
conn, err := pgx.Connect(ctx, base)
if err != nil {
t.Fatal(err)
}
if _, err := conn.Exec(ctx, "CREATE SCHEMA "+schema); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = conn.Exec(context.Background(), "DROP SCHEMA "+schema+" CASCADE")
_ = conn.Close(context.Background())
})
u, err := url.Parse(base)
if err != nil {
t.Fatal(err)
}
q := u.Query()
q.Set("search_path", schema)
u.RawQuery = q.Encode()
return u.String()
}
func freeAddr(t *testing.T) string {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
addr := l.Addr().String()
_ = l.Close()
return addr
}
// syncWriter serializes writes from the server goroutines.
type syncWriter struct {
mu sync.Mutex
w io.Writer
}
func (s *syncWriter) Write(p []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.w.Write(p)
}
// TestServeEndToEnd runs the real server against the test database.
func TestServeEndToEnd(t *testing.T) {
dbURL := testDatabase(t)
dir := t.TempDir()
keyFile := filepath.Join(dir, "master.key")
if code := run(context.Background(), []string{"gen-master-key", "-out", keyFile}, lookupFrom(nil), nil, io.Discard, io.Discard); code != 0 {
t.Fatal("gen-master-key failed")
}
addr := freeAddr(t)
env := map[string]string{
"DDI_HTTP_LISTEN": addr, "DDI_AGENT_API_LISTEN": freeAddr(t), "DDI_DATABASE_URL": dbURL, "DDI_MASTER_KEY_FILE": keyFile, "DDI_STATE_DIR": dir,
"KEYGEN_URL": "http://127.0.0.1:1", "KEYGEN_ACCOUNT_ID": "9de54794-8e94-4250-83f6-87e3d5686a0b",
"KEYGEN_PUBLIC_KEY": "40495568fa239e44736e28949ae2ae93f2c9b6e6bc6d917f4c8cc0eb629e3c62",
}
var out bytes.Buffer
if code := run(context.Background(), []string{"bootstrap-admin", "-username", "root"}, lookupFrom(env), nil, &out, io.Discard); code != 0 {
t.Fatalf("bootstrap-admin failed: %s", out.String())
}
if !strings.Contains(out.String(), "username: root") || !strings.Contains(out.String(), "password: ") {
t.Errorf("bootstrap output %q", out.String())
}
if code := run(context.Background(), []string{"bootstrap-admin"}, lookupFrom(env), nil, io.Discard, io.Discard); code != 1 {
t.Error("second bootstrap must fail")
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan int, 1)
var logs bytes.Buffer
go func() { done <- run(ctx, []string{"serve"}, lookupFrom(env), nil, io.Discard, &syncWriter{w: &logs}) }()
base := fmt.Sprintf("http://%s", addr)
get := func(path string) (int, string) {
t.Helper()
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, base+path, nil)
res, err := http.DefaultClient.Do(req)
if err != nil {
return 0, err.Error()
}
defer res.Body.Close() //nolint:errcheck // test
b, _ := io.ReadAll(res.Body)
return res.StatusCode, string(b)
}
deadline := time.Now().Add(15 * time.Second)
for {
if code, _ := get("/healthz"); code != 0 {
break
}
if time.Now().After(deadline) {
cancel()
t.Fatalf("server did not come up:\n%s", logs.String())
}
time.Sleep(50 * time.Millisecond)
}
for path, want := range map[string]string{
"/readyz": `"database":"ok"`,
"/api/v1/system/info": `"license_state":"trial"`,
"/metrics": "ddi_license_writable 1",
} {
code, body := get(path)
if code != http.StatusOK || !strings.Contains(body, want) {
t.Errorf("%s: %d %q", path, code, body)
}
}
if _, body := get("/metrics"); !strings.Contains(body, `ddi_agents_connected{role="dhcp"} 0`) {
t.Errorf("agent metric missing: %q", body)
}
if code, body := get("/api/v1/networks"); code != http.StatusUnauthorized || !strings.Contains(body, "unauthenticated") {
t.Errorf("unauthenticated API access: %d %q", code, body)
}
cancel()
select {
case code := <-done:
if code != 0 {
t.Errorf("exit code %d:\n%s", code, logs.String())
}
case <-time.After(20 * time.Second):
t.Fatal("server did not shut down")
}
}