test(deploy): cover async progress and access control
CI / validate (pull_request) Canceled after 28s

This commit is contained in:
2026-08-15 19:58:11 +02:00
parent 265136fc30
commit 384c616d58
+154
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"io"
"log/slog"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
@@ -30,6 +31,37 @@ import (
type webLifecycleAgent struct{}
type blockingLifecycleAgent struct {
entered chan struct{}
release chan struct{}
panicOnCreate bool
}
func (a *blockingLifecycleAgent) CreateInstance(_ context.Context, plan agentwire.DeploymentPlan) (agentwire.InstanceState, error) {
close(a.entered)
<-a.release
if a.panicOnCreate {
panic("controlled worker panic")
}
return agentwire.InstanceState{InstanceID: plan.InstanceID, ContainerID: "controlled-container", PlanDigest: plan.PlanDigest, Health: "stopped"}, nil
}
func (a *blockingLifecycleAgent) InspectInstance(_ context.Context, id string) (agentwire.InstanceState, error) {
return agentwire.InstanceState{InstanceID: id}, nil
}
func (a *blockingLifecycleAgent) StartInstance(_ context.Context, id string) (agentwire.InstanceState, error) {
return agentwire.InstanceState{InstanceID: id, ContainerID: "controlled-container", Running: true, Ready: true, Health: "healthy"}, nil
}
func (a *blockingLifecycleAgent) StopInstance(context.Context, string, int) (agentwire.InstanceState, error) {
return agentwire.InstanceState{}, nil
}
func (a *blockingLifecycleAgent) RestartInstance(context.Context, string, int) (agentwire.InstanceState, error) {
return agentwire.InstanceState{}, nil
}
func (a *blockingLifecycleAgent) DeleteContainer(context.Context, string) error { return nil }
func (a *blockingLifecycleAgent) GetInstanceStats(context.Context, string) (agentwire.InstanceStats, error) {
return agentwire.InstanceStats{}, nil
}
func (webLifecycleAgent) CreateInstance(_ context.Context, plan agentwire.DeploymentPlan) (agentwire.InstanceState, error) {
return agentwire.InstanceState{InstanceID: plan.InstanceID, ContainerID: "container-1", PlanDigest: plan.PlanDigest, Health: "stopped"}, nil
}
@@ -80,6 +112,9 @@ func TestInstanceUnbanCapabilityFallback(t *testing.T) {
}
defer db.Close()
repository := sqlite.NewRepository(db)
if err := repository.SetSecretKey(bytes.Repeat([]byte{1}, 32)); err != nil {
t.Fatal(err)
}
snapshots, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatal(err)
@@ -218,6 +253,9 @@ func TestCatalogPreviewAndDraftAPIAuthorization(t *testing.T) {
}
defer db.Close()
repository := sqlite.NewRepository(db)
if err := repository.SetSecretKey(bytes.Repeat([]byte{1}, 32)); err != nil {
t.Fatal(err)
}
snapshots, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatal(err)
@@ -1109,3 +1147,119 @@ func TestDeploymentFormRequiresAdminAndRendersTemplateFields(t *testing.T) {
missing := request(t, handler, http.MethodGet, "/catalog/missing/deploy", adminCookies)
assertStatus(t, missing, http.StatusNotFound)
}
func TestDeploymentHTTPAsyncProgressAndRBAC(t *testing.T) {
ctx := context.Background()
db, err := sqlite.Open(ctx, filepath.Join(t.TempDir(), "dogama.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
repository := sqlite.NewRepository(db)
if err := repository.SetSecretKey(bytes.Repeat([]byte{1}, 32)); err != nil {
t.Fatal(err)
}
snapshots, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatal(err)
}
if err = repository.Sync(ctx, snapshots); err != nil {
t.Fatal(err)
}
authService := auth.New(db)
if err = authService.BootstrapAdmin(ctx, "admin", "correct horse battery staple"); err != nil {
t.Fatal(err)
}
admin, err := authService.Login(ctx, "admin", "correct horse battery staple", "192.0.2.1:1234")
if err != nil {
t.Fatal(err)
}
if _, err = authService.CreateUser(ctx, "viewer", "another correct battery staple", "user"); err != nil {
t.Fatal(err)
}
viewer, err := authService.Login(ctx, "viewer", "another correct battery staple", "192.0.2.2:1234")
if err != nil {
t.Fatal(err)
}
agent := &blockingLifecycleAgent{entered: make(chan struct{}), release: make(chan struct{})}
handler, err := NewHandlerCompleteWithCatalogAndDeployment(authService, repository, instance.NewLifecycleService(repository, agent), nil, nil, nil, nil, nil, filepath.Join(t.TempDir(), "servers"), slog.New(slog.NewTextHandler(io.Discard, nil)))
if err != nil {
t.Fatal(err)
}
var body bytes.Buffer
form := multipart.NewWriter(&body)
for key, value := range map[string]string{"csrf_token": admin.CSRFToken, "display_name": "Async test", "description": "safe", "config_server_name": "Async", "config_server_description": "safe", "config_max_players": "16", "config_admin_password": "top-secret-value", "config_rest_api_enabled": "true", "config_rest_api_port": "8212"} {
_ = form.WriteField(key, value)
}
_ = form.Close()
req := httptest.NewRequest(http.MethodPost, "/catalog/palworld-official/deploy", &body)
req.Header.Set("Content-Type", form.FormDataContentType())
req.Header.Set("Accept", "application/json")
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: admin.Token})
req.AddCookie(&http.Cookie{Name: csrfCookie, Value: admin.CSRFToken})
response := httptest.NewRecorder()
handler.ServeHTTP(response, req)
assertStatus(t, response, http.StatusAccepted)
var accepted map[string]string
if err := json.Unmarshal(response.Body.Bytes(), &accepted); err != nil || accepted["operation_id"] == "" {
t.Fatalf("accepted=%s err=%v", response.Body.String(), err)
}
// The HTTP response has returned while the worker is deterministically blocked.
<-agent.entered
progressReq := httptest.NewRequest(http.MethodGet, "/api/v1/operations/"+accepted["operation_id"], nil)
progressReq.AddCookie(&http.Cookie{Name: sessionCookie, Value: admin.Token})
progress := httptest.NewRecorder()
handler.ServeHTTP(progress, progressReq)
assertStatus(t, progress, http.StatusOK)
if strings.Contains(progress.Body.String(), "top-secret-value") || !strings.Contains(progress.Body.String(), "installation") {
t.Fatalf("unexpected progress body: %s", progress.Body.String())
}
users, _ := authService.ListUsers(ctx)
var viewerID, adminID string
for _, u := range users {
if u.Username == "viewer" {
viewerID = u.ID
}
if u.Username == "admin" {
adminID = u.ID
}
}
if err := repository.SetMembership(ctx, adminID, accepted["instance_id"], viewerID, "user"); err != nil {
t.Fatal(err)
}
viewerProgress := httptest.NewRequest(http.MethodGet, "/api/v1/operations/"+accepted["operation_id"], nil)
viewerProgress.AddCookie(&http.Cookie{Name: sessionCookie, Value: viewer.Token})
viewerResponse := httptest.NewRecorder()
handler.ServeHTTP(viewerResponse, viewerProgress)
assertStatus(t, viewerResponse, http.StatusOK)
if strings.Contains(viewerResponse.Body.String(), "top-secret-value") {
t.Fatal("secret leaked to member progress")
}
diagnostic := httptest.NewRequest(http.MethodGet, "/api/v1/instances/"+accepted["instance_id"]+"/diagnostics", nil)
diagnostic.AddCookie(&http.Cookie{Name: sessionCookie, Value: viewer.Token})
diagnosticResponse := httptest.NewRecorder()
handler.ServeHTTP(diagnosticResponse, diagnostic)
assertStatus(t, diagnosticResponse, http.StatusForbidden)
unknown := httptest.NewRequest(http.MethodGet, "/api/v1/operations/missing", nil)
unknown.AddCookie(&http.Cookie{Name: sessionCookie, Value: admin.Token})
unknownResponse := httptest.NewRecorder()
handler.ServeHTTP(unknownResponse, unknown)
assertStatus(t, unknownResponse, http.StatusNotFound)
close(agent.release)
deadline := time.Now().Add(2 * time.Second)
for {
value, err := repository.GetOperationProgress(ctx, accepted["operation_id"])
if err == nil && value.GlobalStatus == "success" {
break
}
if time.Now().After(deadline) {
t.Fatalf("operation did not finish: %#v %v", value, err)
}
time.Sleep(10 * time.Millisecond)
}
adminDiagnostic := httptest.NewRequest(http.MethodGet, "/api/v1/instances/"+accepted["instance_id"]+"/diagnostics", nil)
adminDiagnostic.AddCookie(&http.Cookie{Name: sessionCookie, Value: admin.Token})
adminDiagnosticResponse := httptest.NewRecorder()
handler.ServeHTTP(adminDiagnosticResponse, adminDiagnostic)
assertStatus(t, adminDiagnosticResponse, http.StatusOK)
}