package management_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" "pipu/api/internal/auth" "pipu/api/internal/buildinfo" "pipu/api/internal/management" "pipu/api/internal/server" ) type bridge struct { calls int body any } func (b *bridge) Call(_ context.Context, _, _ string, body, out any) error { b.calls++ b.body = body return json.Unmarshal([]byte(`{"sources":[],"versions":[],"current_version":"test","job":null,"source_error":"","entries":[],"revision":"r","restart_required":false}`), out) } func TestUpdatesUseRunningBuildIdentity(t *testing.T) { b := &bridge{} router := server.NewRouter(server.Deps{Management: management.NewHandler(b)}) for _, method := range []string{"GET", "POST"} { body := `{"source_id":"origin","channel":"main","_running_commit":"forged","_running_version":"forged"}` request := httptest.NewRequest(method, "/api/v1/admin/server/updates?channel=main&fresh=true", strings.NewReader(body)) request.Header.Set("Content-Type", "application/json") request = request.WithContext(auth.WithPrincipal(request.Context(), auth.Principal{Role: auth.RoleAdmin})) rec := httptest.NewRecorder() router.ServeHTTP(rec, request) require.Less(t, rec.Code, 300, rec.Body.String()) payload, err := json.Marshal(b.body) require.NoError(t, err) var values map[string]any require.NoError(t, json.Unmarshal(payload, &values)) require.Equal(t, buildinfo.Version, values["_running_version"]) require.Equal(t, buildinfo.Revision, values["_running_commit"]) require.Equal(t, "main", values["channel"]) if method == "GET" { require.Equal(t, true, values["fresh"]) } } } func TestServerManagementAuthorization(t *testing.T) { for _, tc := range []struct { name string principal auth.Principal updates, config int }{ {"user", auth.Principal{Role: auth.RoleUser}, 403, 403}, {"moderator", auth.Principal{Role: auth.RoleModerator}, 403, 403}, {"delegate", auth.Principal{Role: auth.RoleUser, ManagesUpdates: true}, 200, 403}, {"admin", auth.Principal{Role: auth.RoleAdmin}, 200, 200}, } { t.Run(tc.name, func(t *testing.T) { b := &bridge{} router := server.NewRouter(server.Deps{Management: management.NewHandler(b)}) for _, op := range []struct { method, path, body string want int }{ {"GET", "updates", "", tc.updates}, {"POST", "updates", `{"source_id":"origin","channel":"main"}`, tc.updates}, {"PUT", "update-sources", `{"sources":[]}`, tc.updates}, {"GET", "config", "", tc.config}, {"PATCH", "config", `{"revision":"r","values":{}}`, tc.config}, {"POST", "config/apply", "", tc.config}, } { request := httptest.NewRequest(op.method, "/api/v1/admin/server/"+op.path, strings.NewReader(op.body)) request.Header.Set("Content-Type", "application/json") request = request.WithContext(auth.WithPrincipal(request.Context(), tc.principal)) rec := httptest.NewRecorder() router.ServeHTTP(rec, request) want := op.want if op.method == "POST" && want == 200 { want = 202 } require.Equal(t, want, rec.Code, rec.Body.String()) } if tc.updates == 403 && tc.config == 403 { require.Zero(t, b.calls) } }) } t.Run("anonymous", func(t *testing.T) { router := server.NewRouter(server.Deps{Management: management.NewHandler(&bridge{})}) rec := httptest.NewRecorder() router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/admin/server/config", nil)) require.Equal(t, 401, rec.Code) }) } func TestMailboxRoundTripAndUnavailable(t *testing.T) { dir := t.TempDir() m := management.Mailbox{Dir: dir} var out map[string]any require.Error(t, m.Call(context.Background(), "actor", "config.get", nil, &out)) require.NoError(t, os.WriteFile(filepath.Join(dir, "heartbeat"), []byte("1"), 0600)) require.Error(t, m.Call(context.Background(), "actor", "config.get", nil, &out)) require.NoError(t, os.WriteFile(filepath.Join(dir, "heartbeat"), []byte(time.Now().Format("invalid")), 0600)) require.Error(t, m.Call(context.Background(), "actor", "config.get", nil, &out)) raw, _ := json.Marshal(time.Now().Unix()) require.NoError(t, os.WriteFile(filepath.Join(dir, "heartbeat"), raw, 0600)) done := make(chan error, 1) go func() { done <- m.Call(context.Background(), "actor", "config.get", nil, &out) }() var requests []string require.Eventually(t, func() bool { requests, _ = filepath.Glob(filepath.Join(dir, "*.request.json")) return len(requests) == 1 }, time.Second, 10*time.Millisecond) request, err := os.ReadFile(requests[0]) require.NoError(t, err) var payload map[string]any require.NoError(t, json.Unmarshal(request, &payload)) require.Equal(t, "config.get", payload["operation"]) require.Equal(t, "actor", payload["actor"]) response := strings.Replace(requests[0], ".request.json", ".response.json", 1) require.NoError(t, os.WriteFile(response, []byte(`{"data":{"revision":"ok"}}`), 0600)) require.NoError(t, <-done) require.Equal(t, "ok", out["revision"]) remaining, _ := filepath.Glob(filepath.Join(dir, "*.json")) require.Empty(t, remaining) }