212 lines
7.5 KiB
Go
212 lines
7.5 KiB
Go
package main
|
|
|
|
// api_prepare_test.go — POST /api/repos/{repo}/prepare (ops prompt routing),
|
|
// GET /api/repos built flags, POST /api/spawn imageUsed resolution.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestAPIPrepareRepoRoutesToOps: with a live ops agent WS the prepare route
|
|
// delivers the prompt; without it the route 409s.
|
|
func TestAPIPrepareRepoRoutesToOps(t *testing.T) {
|
|
useFakeGit(t, fakeGitModeOK)
|
|
f := newFakeDocker()
|
|
tsD := f.server(t)
|
|
t.Setenv("DOCKER_HOST", "tcp://"+tsD.Listener.Addr().String())
|
|
buildCtx := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(buildCtx, "docker"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(buildCtx, "docker", "worker.Dockerfile"), []byte("FROM scratch\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(envWorkerDockerfile, filepath.Join(buildCtx, "docker", "worker.Dockerfile"))
|
|
t.Setenv(envWorkerContext, buildCtx)
|
|
t.Setenv(envRepoDir, t.TempDir())
|
|
t.Setenv(envControlDockerfile, filepath.Join(buildCtx, "docker", "control.Dockerfile"))
|
|
if err := os.WriteFile(filepath.Join(buildCtx, "docker", "control.Dockerfile"), []byte("FROM scratch\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("LVMH_TEST_OPS_BOOT_WAIT", "3s")
|
|
|
|
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")}
|
|
ts := httptest.NewServer(srv.Routes(""))
|
|
t.Cleanup(ts.Close)
|
|
auth := testToken
|
|
prepareURL := ts.URL + "/api/repos/g/p/prepare"
|
|
|
|
// ops offline (and cannot come online — no real agent) → 409 after short wait
|
|
if code, body := apiReq(t, http.MethodPost, prepareURL, auth, ""); code != http.StatusConflict {
|
|
t.Fatalf("prepare offline = %d %s, want 409", code, body)
|
|
}
|
|
|
|
// path validation
|
|
for _, path := range []string{"/api/repos/noslash/prepare", "/api/repos/g/p/notprepare"} {
|
|
if code, _ := apiReq(t, http.MethodPost, ts.URL+path, auth, ""); code != http.StatusBadRequest {
|
|
t.Fatalf("POST %s = %d, want 400", path, code)
|
|
}
|
|
}
|
|
if code, _ := apiReq(t, http.MethodPost, prepareURL, "", ""); code != http.StatusUnauthorized {
|
|
t.Fatal("prepare must require auth")
|
|
}
|
|
|
|
// online ops: prompt delivered on the ops session WS; the reset removes
|
|
// the ops container + wipes its transcript first.
|
|
if err := store.UpsertSession(SessionInfo{ID: opsSessionID}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.AppendEvent(Event{SessionID: opsSessionID, Seq: 1, TS: 1, Type: "message_end", Payload: json.RawMessage(`{}`)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ws := dialAgent(t, ts)
|
|
if err := ws.WriteJSON(helloFrame(opsSessionID)); err != nil {
|
|
t.Fatalf("hello: %v", err)
|
|
}
|
|
if welcome := readFrame(t, ws); welcome["type"] != evWelcome {
|
|
t.Fatalf("welcome = %v", welcome)
|
|
}
|
|
code, body := apiReq(t, http.MethodPost, ts.URL+"/api/repos/g%2Fp/prepare", auth, "")
|
|
if code != http.StatusOK || !strings.Contains(body, `"ok":true`) {
|
|
t.Fatalf("prepare = %d %s, want 200 ok", code, body)
|
|
}
|
|
prompt := readFrame(t, ws)
|
|
if prompt["type"] != evPrompt {
|
|
t.Fatalf("prompt frame = %v", prompt)
|
|
}
|
|
want := "prepare g/p: clone, build a worker image, register it"
|
|
if prompt["message"] != want {
|
|
t.Fatalf("prompt message = %q, want %q", prompt["message"], want)
|
|
}
|
|
// transcript wiped by the reset
|
|
evs, err := store.EventsAfter(opsSessionID, 0, 100)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(evs) != 0 {
|
|
t.Fatalf("ops transcript not cleared: %d events", len(evs))
|
|
}
|
|
}
|
|
|
|
func TestAPIRepoImagesBuiltFlag(t *testing.T) {
|
|
ts, f := newSpawnAPIServer(t)
|
|
auth := testToken
|
|
custom := "lvmh-worker-group--project-ab12cd"
|
|
f.imageTags = map[string]bool{imageRefWorker: true, custom: true}
|
|
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/group/project/image", auth, `{"image":"`+custom+`"}`); code != http.StatusOK {
|
|
t.Fatal("register custom image failed")
|
|
}
|
|
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/other/repo/image", auth, `{"image":"lvmh-worker-other"}`); code != http.StatusOK {
|
|
t.Fatal("register second image failed")
|
|
}
|
|
|
|
code, body := apiReq(t, http.MethodGet, ts.URL+"/api/repos", auth, "")
|
|
if code != http.StatusOK {
|
|
t.Fatalf("repos = %d %s", code, body)
|
|
}
|
|
var rows []RepoImageRow
|
|
if err := json.Unmarshal([]byte(body), &rows); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("rows = %+v", rows)
|
|
}
|
|
for _, r := range rows {
|
|
wantBuilt := r.Image == custom
|
|
if r.Built != wantBuilt {
|
|
t.Fatalf("row %+v built = %v, want %v", r, r.Built, wantBuilt)
|
|
}
|
|
}
|
|
|
|
// docker unreachable → 500, not a silent built=false
|
|
f.failImages = true
|
|
if code, body := apiReq(t, http.MethodGet, ts.URL+"/api/repos", auth, ""); code != http.StatusInternalServerError {
|
|
t.Fatalf("repos with dead docker = %d %s, want 500", code, body)
|
|
}
|
|
}
|
|
|
|
// TestAPISpawnImageUsed: the spawn response names the image the job will use
|
|
// (registry lookup), default or custom; the running job line carries it too.
|
|
func TestAPISpawnImageUsed(t *testing.T) {
|
|
ts, f := newSpawnAPIServer(t)
|
|
auth := testToken
|
|
custom := "lvmh-worker-group--project-ab12cd"
|
|
f.imageTags = map[string]bool{imageRefWorker: true, custom: true}
|
|
|
|
// unregistered repo → default worker image
|
|
code, body := apiReq(t, http.MethodPost, ts.URL+"/api/spawn", auth, `{"repo":"plain/repo"}`)
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("default spawn = %d %s", code, body)
|
|
}
|
|
var res SpawnResult
|
|
if err := json.Unmarshal([]byte(body), &res); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if res.ImageUsed != imageRefWorker {
|
|
t.Fatalf("default imageUsed = %q, want %q", res.ImageUsed, imageRefWorker)
|
|
}
|
|
|
|
// registered + built custom image → custom in response and job line
|
|
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/group/project/image", auth, `{"image":"`+custom+`"}`); code != http.StatusOK {
|
|
t.Fatal("register custom failed")
|
|
}
|
|
code, body = apiReq(t, http.MethodPost, ts.URL+"/api/spawn", auth, `{"repo":"group/project"}`)
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("custom spawn = %d %s", code, body)
|
|
}
|
|
if err := json.Unmarshal([]byte(body), &res); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if res.ImageUsed != custom {
|
|
t.Fatalf("custom imageUsed = %q, want %q", res.ImageUsed, custom)
|
|
}
|
|
|
|
// running job snapshot reports the image
|
|
var runningJob SpawnJob
|
|
waitFor(t, 5*time.Second, func() bool {
|
|
_, body := apiReq(t, http.MethodGet, ts.URL+"/api/spawn/status", auth, "")
|
|
var jobs []SpawnJob
|
|
_ = json.Unmarshal([]byte(body), &jobs)
|
|
for _, j := range jobs {
|
|
if j.SessionID == res.SessionID && j.State == stateRunning {
|
|
runningJob = j
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
if runningJob.Message != "image "+custom {
|
|
t.Fatalf("running message = %q, want %q", runningJob.Message, "image "+custom)
|
|
}
|
|
|
|
// registered but NOT built: response still names the custom image (the
|
|
// job itself errors later with the ask-ops message)
|
|
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/other/repo/image", auth, `{"image":"lvmh-worker-unbuilt"}`); code != http.StatusOK {
|
|
t.Fatal("register unbuilt failed")
|
|
}
|
|
code, body = apiReq(t, http.MethodPost, ts.URL+"/api/spawn", auth, `{"repo":"other/repo"}`)
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("unbuilt spawn = %d %s", code, body)
|
|
}
|
|
var res2 SpawnResult
|
|
_ = json.Unmarshal([]byte(body), &res2)
|
|
if res2.ImageUsed != "lvmh-worker-unbuilt" {
|
|
t.Fatalf("unbuilt imageUsed = %q, want lvmh-worker-unbuilt", res2.ImageUsed)
|
|
}
|
|
}
|