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) } }