Files
lvmh/daemon/pi_config_test.go
T

309 lines
10 KiB
Go

package main
// pi_config_test.go — resync endpoint wiring: auth, conflict-while-running,
// missing dotfiles mount, missing script.
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
func TestAPIPiConfigResync(t *testing.T) {
ts, _ := newTestServer(t)
auth := testToken
if code, _ := apiReq(t, http.MethodPost, ts.URL+"/api/pi-config/resync", "", ""); code != http.StatusUnauthorized {
t.Fatalf("resync without token = %d, want 401", code)
}
// script missing in test env (path is /app/build/...): 500 before any work
t.Setenv(envPiDotfilesMount, t.TempDir())
code, body := apiReq(t, http.MethodPost, ts.URL+"/api/pi-config/resync", auth, "")
if code != http.StatusInternalServerError && code != http.StatusBadRequest {
t.Fatalf("resync with no settings/script = %d %s", code, body)
}
}
func TestPiSyncStateMutex(t *testing.T) {
piSyncState.mu.Lock()
if piSyncState.running {
piSyncState.mu.Unlock()
t.Fatal("running must start false")
}
piSyncState.running = true
piSyncState.mu.Unlock()
// emulate handler guard
piSyncState.mu.Lock()
again := piSyncState.running
piSyncState.mu.Unlock()
if !again {
t.Fatal("guard must observe running")
}
piSyncState.mu.Lock()
piSyncState.running = false
piSyncState.mu.Unlock()
}
func TestCopyTreeSmoke(t *testing.T) {
src := t.TempDir()
dst := t.TempDir() + "/out"
if err := os.WriteFile(src+"/f.txt", []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := copyTree(src, dst); err != nil {
t.Fatalf("copyTree: %v", err)
}
if _, err := os.Stat(dst + "/f.txt"); err != nil {
t.Fatalf("copied file missing: %v", err)
}
}
func TestPiConfigResyncHappyPathStub(t *testing.T) {
// full resync against a stub script + fake docker + writable dirs
useFakeGit(t, fakeGitModeOK)
f := newFakeDocker()
ts := f.server(t)
t.Setenv("DOCKER_HOST", "tcp://"+ts.Listener.Addr().String())
root := t.TempDir()
src := filepath.Join(root, "dotfiles")
bake := filepath.Join(root, "bake")
repo := filepath.Join(root, "repo")
for _, d := range []string{src, bake, filepath.Join(repo, "docker")} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(src, "settings.json"), []byte(`{"packages":[]}`), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repo, "docker", "worker.Dockerfile"), []byte("FROM scratch\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repo, "docker", "control.Dockerfile"), []byte("FROM scratch\n"), 0o644); err != nil {
t.Fatal(err)
}
script := filepath.Join(root, "sync.sh")
if err := os.WriteFile(script, []byte("#!/bin/sh\nmkdir -p \"$LVMH_PI_BAKE_DIR\"\ncp \"$LVMH_PI_AGENT_DIR/settings.json\" \"$LVMH_PI_BAKE_DIR/\"\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv(envPiDotfilesMount, src)
t.Setenv("LVMH_PI_BAKE_DIR", bake)
daemonToken = testToken
store := openTestStore(t)
hub := NewHub(store)
sp, err := NewSpawner(context.Background(), store, hub, "https://gitlab.example")
if err != nil {
t.Fatal(err)
}
srv := &Server{store: store, hub: hub, spawn: sp, gitlab: NewGitLab(store, "https://gitlab.example")}
api := httptest.NewServer(srv.Routes(""))
t.Cleanup(api.Close)
// point the handler at the stub script + repo layout
t.Setenv("LVMH_TEST_SYNC_SCRIPT", script)
t.Setenv("LVMH_TEST_BUILD_CTX", filepath.Join(root, "bake-ctx"))
t.Setenv("LVMH_TEST_BUILD_REPO", repo)
if code, body := apiReq(t, http.MethodPost, api.URL+"/api/pi-config/resync", testToken, ""); code != http.StatusOK {
t.Fatalf("resync = %d %s", code, body)
}
if !f.hasCall(http.MethodPost, "/build") {
t.Fatal("image builds not issued")
}
}
func TestPiConfigResyncConflictWhileRunning(t *testing.T) {
ts, _ := newTestServer(t)
piSyncState.mu.Lock()
piSyncState.running = true
piSyncState.mu.Unlock()
t.Cleanup(func() {
piSyncState.mu.Lock()
piSyncState.running = false
piSyncState.mu.Unlock()
})
if code, body := apiReq(t, http.MethodPost, ts.URL+"/api/pi-config/resync", testToken, ""); code != http.StatusConflict {
t.Fatalf("resync while running = %d %s, want 409", code, body)
}
}
func TestCopyTreeError(t *testing.T) {
if err := copyTree(t.TempDir()+"/nope", t.TempDir()+"/dst"); err == nil {
t.Fatal("copyTree of missing src must fail")
}
}
func TestPiConfigResyncFailurePaths(t *testing.T) {
ts, _ := newTestServer(t)
// 1: dotfiles mount without settings.json -> 400
empty := t.TempDir()
t.Setenv(envPiDotfilesMount, empty)
t.Setenv("LVMH_TEST_SYNC_SCRIPT", "/nonexistent-sync.sh")
if code, body := apiReq(t, http.MethodPost, ts.URL+"/api/pi-config/resync", testToken, ""); code != http.StatusBadRequest {
t.Fatalf("no settings = %d %s, want 400", code, body)
}
// 2: settings present, script missing -> 500
if err := os.WriteFile(filepath.Join(empty, "settings.json"), []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
if code, body := apiReq(t, http.MethodPost, ts.URL+"/api/pi-config/resync", testToken, ""); code != http.StatusInternalServerError {
t.Fatalf("missing script = %d %s, want 500", code, body)
}
// 3: script fails -> 500 sync failed
root := t.TempDir()
src := filepath.Join(root, "src")
if err := os.MkdirAll(src, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "settings.json"), []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
failScript := filepath.Join(root, "fail.sh")
if err := os.WriteFile(failScript, []byte("#!/bin/sh\nexit 3\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv(envPiDotfilesMount, src)
t.Setenv("LVMH_TEST_SYNC_SCRIPT", failScript)
if code, body := apiReq(t, http.MethodPost, ts.URL+"/api/pi-config/resync", testToken, ""); code != http.StatusInternalServerError {
t.Fatalf("failing script = %d %s, want 500", code, body)
}
}
func TestPiConfigResyncBuildFailure(t *testing.T) {
useFakeGit(t, fakeGitModeOK)
f := newFakeDocker()
f.failBuildHTTP = true
ts := f.server(t)
t.Setenv("DOCKER_HOST", "tcp://"+ts.Listener.Addr().String())
root := t.TempDir()
src := filepath.Join(root, "src")
repo := filepath.Join(root, "repo")
bake := filepath.Join(root, "bake")
for _, d := range []string{src, bake, filepath.Join(repo, "docker")} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(src, "settings.json"), []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repo, "docker", "worker.Dockerfile"), []byte("FROM scratch\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repo, "docker", "control.Dockerfile"), []byte("FROM scratch\n"), 0o644); err != nil {
t.Fatal(err)
}
script := filepath.Join(root, "ok.sh")
if err := os.WriteFile(script, []byte("#!/bin/sh\nmkdir -p \"$LVMH_PI_BAKE_DIR\"\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv(envPiDotfilesMount, src)
t.Setenv("LVMH_TEST_SYNC_SCRIPT", script)
t.Setenv("LVMH_PI_BAKE_DIR", bake)
t.Setenv("LVMH_TEST_BUILD_REPO", repo)
t.Setenv("LVMH_TEST_BUILD_CTX", filepath.Join(root, "ctx"))
daemonToken = testToken
store := openTestStore(t)
hub := NewHub(store)
sp, err := NewSpawner(context.Background(), store, hub, "https://gitlab.example")
if err != nil {
t.Fatal(err)
}
srv := &Server{store: store, hub: hub, spawn: sp, gitlab: NewGitLab(store, "https://gitlab.example")}
api := httptest.NewServer(srv.Routes(""))
t.Cleanup(api.Close)
if code, body := apiReq(t, http.MethodPost, api.URL+"/api/pi-config/resync", testToken, ""); code != http.StatusInternalServerError {
t.Fatalf("build failure = %d %s, want 500", code, body)
}
}
func mustWrite(t *testing.T, path, content string, mode os.FileMode) string {
t.Helper()
if err := os.WriteFile(path, []byte(content), mode); err != nil {
t.Fatal(err)
}
return path
}
func TestPiConfigResyncContextFailureArms(t *testing.T) {
root := t.TempDir()
src := filepath.Join(root, "src")
if err := os.MkdirAll(src, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "settings.json"), []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
okScript := filepath.Join(root, "ok.sh")
if err := os.WriteFile(okScript, []byte("#!/bin/sh\nmkdir -p \"$LVMH_PI_BAKE_DIR\"\n"), 0o755); err != nil {
t.Fatal(err)
}
// repo missing control.Dockerfile only → copyTree ok, rename ok, worker ok (scratch), ops dockerfile missing → buildImage fails
useFakeGit(t, fakeGitModeOK)
f := newFakeDocker()
ts := f.server(t)
t.Setenv("DOCKER_HOST", "tcp://"+ts.Listener.Addr().String())
daemonToken = testToken
store := openTestStore(t)
hub := NewHub(store)
sp, err := NewSpawner(context.Background(), store, hub, "https://gitlab.example")
if err != nil {
t.Fatal(err)
}
srv := &Server{store: store, hub: hub, spawn: sp, gitlab: NewGitLab(store, "https://gitlab.example")}
api := httptest.NewServer(srv.Routes(""))
t.Cleanup(api.Close)
repo := filepath.Join(root, "repo")
bake := filepath.Join(root, "bake")
for _, d := range []string{bake, filepath.Join(repo, "docker")} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(repo, "docker", "worker.Dockerfile"), []byte("FROM scratch\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repo, "docker", "control.Dockerfile"), []byte("FROM scratch\n"), 0o644); err != nil {
t.Fatal(err)
}
// rename arm: bake dir absent → os.Rename fails
t.Setenv("LVMH_TEST_SYNC_SCRIPT", mustWrite(t, filepath.Join(root, "noop.sh"), "#!/bin/sh\nexit 0\n", 0o755))
t.Setenv(envPiDotfilesMount, src)
t.Setenv("LVMH_PI_BAKE_DIR", filepath.Join(root, "no-such-bake"))
t.Setenv("LVMH_TEST_BUILD_REPO", repo)
t.Setenv("LVMH_TEST_BUILD_CTX", filepath.Join(root, "ctx"))
if code, body := apiReq(t, http.MethodPost, api.URL+"/api/pi-config/resync", testToken, ""); code != http.StatusInternalServerError {
t.Fatalf("ops missing = %d %s, want 500", code, body)
}
}
func TestPiConfigDefaultScriptPathBranch(t *testing.T) {
ts, _ := newTestServer(t)
root := t.TempDir()
src := filepath.Join(root, "src")
if err := os.MkdirAll(src, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "settings.json"), []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
t.Setenv(envPiDotfilesMount, src)
t.Setenv("LVMH_TEST_SYNC_SCRIPT", "") // default branch → missing real script path → 500
if code, body := apiReq(t, http.MethodPost, ts.URL+"/api/pi-config/resync", testToken, ""); code != http.StatusInternalServerError {
t.Fatalf("default script missing = %d %s, want 500", code, body)
}
}