test(deploy): cover async progress and access control
CI / validate (pull_request) Canceled after 28s
CI / validate (pull_request) Canceled after 28s
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user