174 lines
8.3 KiB
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)
|
|
}
|
|
}
|