vdi-cloud/platform/internal/server/policies_test.go

174 lines
8.3 KiB
Go

package server
import (
"fmt"
"testing"
)
func policyTemplate(t *testing.T, f *fixture, admin string) string {
t.Helper()
status, out := f.request("POST", "/terminal-policy-templates", admin, map[string]any{"name": "办公策略", "version": "1.0", "heartbeatInterval": 30, "commandPollInterval": 5, "desktopRefreshInterval": 20}, "")
expect(t, 201, status, out)
return out["id"].(string)
}
func policyCap(t *testing.T, f *fixture, device string, protocol int) {
t.Helper()
status, out := f.request("POST", "/terminal/heartbeat", device, map[string]any{"agentVersion": "qt-platform/0.3.0", "policyProtocol": protocol}, "")
expect(t, 200, status, out)
}
func TestPolicyDispatchActualReceiptsAndHistory(t *testing.T) {
f := newFixture(t)
admin := f.login("admin")
id, device, _ := lifecycleDevice(t, f, admin, "POLICY-001")
template := policyTemplate(t, f, admin)
status, out := f.request("GET", "/terminal/config", device, nil, "")
expect(t, 200, status, out)
if out["heartbeatInterval"] != float64(30) || out["configVersion"] != float64(1) || out["deploymentId"] != "" {
t.Fatal(out)
}
body := map[string]any{"templateId": template, "targetMode": "devices", "terminalIds": []string{id}}
status, out = f.request("POST", "/terminal-policy-deployments", admin, body, "policy-1")
expect(t, 409, status, out)
policyCap(t, f, device, 1)
// Offline capable devices must queue without pretending to have applied.
if _, err := f.db.Exec("UPDATE terminals SET last_seen=now()-interval '5 minutes' WHERE id=$1", id); err != nil {
t.Fatal(err)
}
status, first := f.request("POST", "/terminal-policy-deployments", admin, body, "policy-1")
expect(t, 202, status, first)
deployment := first["id"].(string)
status, out = f.request("POST", "/terminal-policy-deployments", admin, body, "policy-1")
expect(t, 202, status, out)
if out["id"] != deployment {
t.Fatal("duplicate dispatch", out)
}
status, out = f.request("GET", "/terminal-policy-deployments/"+deployment+"/results", admin, nil, "")
expect(t, 200, status, out)
row := out["items"].([]any)[0].(map[string]any)
if row["status"] != "pending" || row["online"] != false || row["acknowledgedAt"] != nil {
t.Fatal(row)
}
status, out = f.request("GET", "/terminal/config", device, nil, "")
expect(t, 200, status, out)
if out["commandPollInterval"] != float64(5) || out["desktopRefreshInterval"] != float64(20) || out["deploymentId"] != deployment {
t.Fatal(out)
}
version := out["configVersion"]
ack := map[string]any{"deploymentId": deployment, "configVersion": version, "status": "failed", "errorCode": "persist_failed"}
status, out = f.request("POST", "/terminal/config/ack", device, ack, "")
expect(t, 200, status, out)
status, out = f.request("GET", "/terminal-policy-deployments/"+deployment+"/results", admin, nil, "")
expect(t, 200, status, out)
row = out["items"].([]any)[0].(map[string]any)
if row["status"] != "failed" || row["errorCode"] != "persist_failed" {
t.Fatal(row)
}
delete(ack, "errorCode")
ack["status"] = "applied"
status, out = f.request("POST", "/terminal/config/ack", device, ack, "")
expect(t, 200, status, out)
status, out = f.request("POST", "/terminal/config/ack", device, ack, "")
expect(t, 200, status, out)
// A later deployment gets a new revision, even when restoring the same template.
status, second := f.request("POST", "/terminal-policy-deployments", admin, body, "policy-2")
expect(t, 202, status, second)
status, out = f.request("POST", "/terminal/config/ack", device, ack, "")
expect(t, 409, status, out)
status, out = f.request("GET", "/terminal-policy-deployments/"+deployment+"/results", admin, nil, "")
expect(t, 200, status, out)
row = out["items"].([]any)[0].(map[string]any)
if row["status"] != "applied" || row["configVersion"] != version {
t.Fatal("history changed", row)
}
status, out = f.request("GET", "/terminal-policy-deployments/"+second["id"].(string)+"/results", admin, nil, "")
expect(t, 200, status, out)
row = out["items"].([]any)[0].(map[string]any)
if row["status"] != "pending" || row["configVersion"] == version {
t.Fatal(row)
}
// Resetting identity requires a fresh application and capability report.
status, out = f.request("POST", "/terminals/"+id+"/reactivate", admin, nil, "")
expect(t, 200, status, out)
status, out = f.request("POST", "/terminal/config/ack", device, ack, "")
expect(t, 401, status, out)
status, out = f.request("POST", "/terminal-policy-deployments", admin, body, "policy-3")
expect(t, 409, status, out)
}
func TestPolicyValidationRBACAndGroupSnapshot(t *testing.T) {
f := newFixture(t)
admin := f.login("admin")
viewer := f.login("viewer")
operator := f.login("operator")
template := policyTemplate(t, f, admin)
valid := map[string]any{"name": "办公策略", "version": "1.0", "heartbeatInterval": 30, "commandPollInterval": 5, "desktopRefreshInterval": 20}
status, out := f.request("POST", "/terminal-policy-templates", admin, valid, "")
expect(t, 409, status, out)
for _, field := range []string{"heartbeatInterval", "commandPollInterval", "desktopRefreshInterval"} {
in := map[string]any{}
for k, v := range valid {
in[k] = v
}
in["version"] = "invalid-" + field
in[field] = 0
status, out = f.request("POST", "/terminal-policy-templates", admin, in, "")
expect(t, 400, status, out)
}
id, device, _ := lifecycleDevice(t, f, admin, "POLICY-GROUP-001")
policyCap(t, f, device, 1)
body := map[string]any{"templateId": template, "targetMode": "group", "group": "原分组"}
for _, role := range []string{viewer, operator} {
status, out = f.request("POST", "/terminal-policy-templates", role, valid, "")
expect(t, 403, status, out)
status, out = f.request("POST", "/terminal-policy-deployments", role, body, "rbac-policy")
expect(t, 403, status, out)
for _, path := range []string{"/terminal-policy-templates", "/terminal-policy-states", "/terminal-policy-deployments"} {
status, out = f.request("GET", path, role, nil, "")
expect(t, 200, status, out)
}
}
status, out = f.request("POST", "/terminal-policy-deployments", admin, body, "")
expect(t, 400, status, out)
status, first := f.request("POST", "/terminal-policy-deployments", admin, body, "group-1")
expect(t, 202, status, first)
_, late, _ := lifecycleDevice(t, f, admin, "POLICY-GROUP-002")
policyCap(t, f, late, 1)
status, out = f.request("GET", "/terminal-policy-deployments/"+first["id"].(string)+"/results", viewer, nil, "")
expect(t, 200, status, out)
if len(out["items"].([]any)) != 1 {
t.Fatal("group membership retroactively changed", out)
}
body["group"] = "不存在"
status, out = f.request("POST", "/terminal-policy-deployments", admin, body, "group-1")
expect(t, 409, status, out)
status, out = f.request("POST", "/terminal-policy-deployments", admin, body, "empty")
expect(t, 400, status, out)
for i, bad := range []map[string]any{
{"templateId": template, "targetMode": "devices", "terminalIds": []string{id, "missing"}},
{"templateId": template, "targetMode": "devices", "terminalIds": []string{id}, "group": "原分组"},
{"templateId": template, "targetMode": "group", "group": "原分组", "terminalIds": []string{id}},
} {
status, out = f.request("POST", "/terminal-policy-deployments", admin, bad, fmt.Sprint("invalid", i))
expect(t, 400, status, out)
}
// Downgraded clients explicitly revoke their capability on the next heartbeat.
policyCap(t, f, late, 0)
body["group"] = "原分组"
status, out = f.request("POST", "/terminal-policy-deployments", admin, body, "unsupported")
expect(t, 409, status, out)
var revision int
if err := f.db.Get(&revision, "SELECT config_version FROM terminals WHERE id=$1", id); err != nil || revision != 2 {
t.Fatal("partial group dispatch", revision, err)
}
status, out = f.request("POST", "/terminal/config/ack", device, map[string]any{"deploymentId": first["id"], "configVersion": 2, "status": "failed", "errorCode": "token=secret"}, "")
expect(t, 400, status, out)
// Replacing an unapplied configuration preserves that outcome as superseded.
status, second := f.request("POST", "/terminal-policy-deployments", admin, map[string]any{"templateId": template, "targetMode": "devices", "terminalIds": []string{id}}, "replace")
expect(t, 202, status, second)
status, out = f.request("GET", "/terminal-policy-deployments/"+first["id"].(string)+"/results", admin, nil, "")
expect(t, 200, status, out)
if out["items"].([]any)[0].(map[string]any)["status"] != "superseded" {
t.Fatal(out)
}
}