ci / api / api (push) Successful in 50s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m0s
ci / db / db (push) Successful in 58s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m27s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m39s
ci / ts / ts (packages + web) (push) Successful in 2m40s
136 lines
5.1 KiB
Go
136 lines
5.1 KiB
Go
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)
|
|
}
|