package activator import ( "encoding/json" "errors" "io" "net/http" "os" "testing" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/instanceupdates" "github.com/carlosframework/platform/internal/release" ) // updatesGet asks GET /updates on a control socket with bearer ("" sends none). func updatesGet(t *testing.T, ctl, bearer string) (int, instanceupdates.Report) { t.Helper() req, err := http.NewRequest("GET", "http://carlos/updates?host=other.example.com", nil) if err != nil { t.Fatal(err) } if bearer != "" { req.Header.Set("Authorization", "Bearer "+bearer) } resp, err := socketClient(ctl).Do(req) if err != nil { t.Fatalf("dial control socket %s: %v", ctl, err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var rep instanceupdates.Report _ = json.Unmarshal(body, &rep) return resp.StatusCode, rep } func fakeUpdatesReader(host string) (instanceupdates.Report, error) { return instanceupdates.Report{Settings: release.UpdateSettings{Updates: release.UpdatesManual}, Installed: "v1.2.0"}, nil } // With UpdatesReader set, GET /updates is served on the spawned instance's // control socket behind the minted token, for the host the spawn was for (a // ?host= on the request changes nothing); with Timers left nil the schedule // verbs are still absent. func TestExecBackendStartMountsUpdatesOnControlSocket(t *testing.T) { b := testExecBackend(t) // b.Timers nil b.UpdatesReader = fakeUpdatesReader ctl, token, stop := startAndReachControlSocket(t, b, "upd.example.com") defer stop() code, rep := updatesGet(t, ctl, token) if code != http.StatusOK || rep.Host != "upd.example.com" || rep.Installed != "v1.2.0" || rep.Settings.Updates != release.UpdatesManual { t.Fatalf("GET /updates with the minted token = %d %+v", code, rep) } if code, _ := updatesGet(t, ctl, "not-the-token"); code != http.StatusUnauthorized { t.Fatalf("wrong bearer = %d, want 401", code) } if code, _ := updatesGet(t, ctl, ""); code != http.StatusUnauthorized { t.Fatalf("no bearer = %d, want 401", code) } req, _ := http.NewRequest("GET", "http://carlos/schedule", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := socketClient(ctl).Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("GET /schedule with nil Timers = %d, want 404", resp.StatusCode) } } // A reader that errors (a host the registry has no row for) is a 503 on the // wire, not an empty 200. func TestExecBackendStartUpdatesReaderErrorIs503(t *testing.T) { b := testExecBackend(t) b.UpdatesReader = func(string) (instanceupdates.Report, error) { return instanceupdates.Report{}, errors.New("no route") } ctl, token, stop := startAndReachControlSocket(t, b, "upd503.example.com") defer stop() if code, _ := updatesGet(t, ctl, token); code != http.StatusServiceUnavailable { t.Fatalf("GET /updates = %d, want 503", code) } } // With UpdatesReader nil, even with the schedule verbs mounted, /updates is // not served. func TestExecBackendStartWithNilUpdatesReaderMountsNoUpdates(t *testing.T) { b := testExecBackend(t) b.Timers = &fakeTimerStore{} ctl, token, stop := startAndReachControlSocket(t, b, "noupd.example.com") defer stop() if code, _ := updatesGet(t, ctl, token); code != http.StatusNotFound { t.Fatalf("GET /updates with nil UpdatesReader = %d, want 404", code) } } // The nested layout's listener (prepareAt) mounts the same verb. func TestPreparedControlListenerMountsUpdates(t *testing.T) { b, rt := nestedBackend(t, &fakeControl{}, &fakeUnit{}) b.GenContainment = true b.UpdatesReader = fakeUpdatesReader p0 := b.pathsFor(rt) if err := os.MkdirAll(p0.runDir, 0o750); err != nil { t.Fatal(err) } sock := nestedGenSocketIn(p0.runDir, 7) p, ctl, logs, _, err := b.prepareAt(rt, instancectl.RoleRun, sock, 7) if err != nil { t.Fatalf("prepareAt: %v", err) } if logs != nil { defer logs.Close() } if ctl == nil { t.Fatal("prepareAt bound no control listener") } defer ctl.Close() env, err := readEnvFileBeneath(p.dataDir, rt.Host+".env") if err != nil { t.Fatal(err) } code, rep := updatesGet(t, sock+".control", env["CARLOS_ADMIN_TOKEN"]) if code != http.StatusOK || rep.Host != rt.Host || rep.Installed != "v1.2.0" { t.Fatalf("GET /updates on the prepared listener = %d %+v", code, rep) } }