An upgrade replaces the binary and leaves server.yaml alone, so a control plane installed before guardrails stayed unguarded. Run again, init now appends the baseline guard section (and so fetches the judge) when the file has no guard key at all, keeping the old file as server.yaml.bak-<time>. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
177 lines
5.3 KiB
Go
177 lines
5.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
func newTestInit(t *testing.T) (*serverInit, *bytes.Buffer) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
var out bytes.Buffer
|
|
return &serverInit{
|
|
etcDir: filepath.Join(root, "etc"),
|
|
stateDir: filepath.Join(root, "state"),
|
|
pkiDir: filepath.Join(root, "etc", "pki"),
|
|
admin: "admin@example.com",
|
|
out: &out,
|
|
}, &out
|
|
}
|
|
|
|
func TestInitLeavesAControlPlaneThatStarts(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
|
|
cfg, err := config.Load(in.configPath())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The configuration install.md used to show had an empty catalog and no
|
|
// control.enabled, which the server refuses as having nothing to serve.
|
|
if !cfg.ControlPlaneMode() {
|
|
t.Error("the written configuration does not run a control plane")
|
|
}
|
|
if !cfg.AgentTLS.Enabled() || !cfg.Auth.Enabled {
|
|
t.Error("mutual TLS and authentication should both be on")
|
|
}
|
|
for _, f := range []string{"ca.pem", "server.pem", "server-key.pem"} {
|
|
if _, err := os.Stat(filepath.Join(in.etcDir, f)); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
s, err := store.Open(context.Background(), cfg.Auth.Store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
u, err := s.UserByEmail(context.Background(), "admin@example.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inDefault, _ := s.CanUseProject(context.Background(), u.ID, "default")
|
|
if !u.IsAdmin || u.State != store.UserActive || !inDefault {
|
|
t.Errorf("administrator not ready to use: %+v", u)
|
|
}
|
|
if !strings.Contains(out.String(), "password: ") {
|
|
t.Error("the generated password was not shown")
|
|
}
|
|
}
|
|
|
|
func TestInitAgainChangesNothing(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
cert, _ := os.ReadFile(filepath.Join(in.etcDir, "server.pem"))
|
|
|
|
again := *in
|
|
again.changed = false
|
|
out.Reset()
|
|
if err := again.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
if again.changed {
|
|
t.Errorf("a second run changed something:\n%s", out)
|
|
}
|
|
if strings.Contains(out.String(), "password: ") {
|
|
t.Error("a second run must not reset the administrator's password")
|
|
}
|
|
if now, _ := os.ReadFile(filepath.Join(in.etcDir, "server.pem")); !bytes.Equal(now, cert) {
|
|
t.Error("the certificate was reissued without reason")
|
|
}
|
|
}
|
|
|
|
func TestInitWithANewHostKeepsTheOldOnes(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
in.hosts = []string{"old.internal"}
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
in.hosts = []string{"new.internal"}
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
cert, err := readCert(filepath.Join(in.etcDir, "server.pem"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, h := range []string{"old.internal", "new.internal"} {
|
|
if !slices.Contains(cert.DNSNames, h) {
|
|
t.Errorf("certificate lost %s: %v", h, cert.DNSNames)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInitNeverOverwritesTheConfiguration(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
if err := os.MkdirAll(in.etcDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mine := "# mine\ncontrol:\n enabled: true\nauth:\n enabled: true\n store: " +
|
|
filepath.Join(in.stateDir, "mine.db") + "\n"
|
|
if err := os.WriteFile(in.configPath(), []byte(mine), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
// Kept as written; only the guardrails it predates are added after it.
|
|
got, _ := os.ReadFile(in.configPath())
|
|
if !strings.HasPrefix(string(got), mine) || !strings.Contains(string(got[len(mine):]), "\nguard:\n") {
|
|
t.Errorf("server.yaml is not the old file with guard: after it:\n%s", got)
|
|
}
|
|
// And the store it names is the one used.
|
|
if _, err := os.Stat(filepath.Join(in.stateDir, "mine.db")); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestTheInitUnitIsTheShippedUnit(t *testing.T) {
|
|
b, err := os.ReadFile("../../deploy/systemd/llmgw-server.service")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(b) != serverUnit {
|
|
t.Error("deploy/systemd/llmgw-server.service and the unit 'server init' writes have drifted")
|
|
}
|
|
}
|
|
|
|
func TestInitKeepsCertificatesFromAnAuthorityElsewhere(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
// Issued on another machine by an authority that never comes here.
|
|
elsewhere := &serverInit{etcDir: in.etcDir, pkiDir: filepath.Join(t.TempDir(), "pki"), out: &bytes.Buffer{}}
|
|
if err := os.MkdirAll(in.etcDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := elsewhere.ensureCerts(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, _ := os.ReadFile(filepath.Join(in.etcDir, "server.pem"))
|
|
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
if after, _ := os.ReadFile(filepath.Join(in.etcDir, "server.pem")); !bytes.Equal(before, after) {
|
|
t.Error("a certificate from another authority was replaced")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(in.pkiDir, "ca-key.pem")); err == nil {
|
|
t.Error("an authority was created beside one that already exists elsewhere")
|
|
}
|
|
|
|
in.hosts = []string{"new.internal"}
|
|
if err := in.run(context.Background()); err == nil {
|
|
t.Error("--host cannot be honoured without the authority, and should say so")
|
|
}
|
|
}
|