simplify SaaS SIP configuration response

This commit is contained in:
2026-09-28 16:06:24 +08:00
parent bd2e5ad2b1
commit efb2b86f5e
23 changed files with 700 additions and 169 deletions
+26 -19
View File
@@ -38,10 +38,6 @@ type Snapshot struct {
TenantID string
TenantKey string
SIPRevision int64
SIPSnapshotSHA256 string
SIPCellID string
SIPArtifactRevision int64
SIPArtifactConfigSHA256 string
TaskRevision int64
TaskStatus string
TaskMaxConcurrentCalls int64
@@ -122,10 +118,23 @@ func (c *Client) ReadTask(ctx context.Context, taskID, tenantID string) (Snapsho
if err := json.Unmarshal(sipBody, &sip); err != nil {
return Snapshot{}, fmt.Errorf("decode SIP configuration identity: %w", err)
}
if sip.Resource != resourceSIPConfig || sip.DispatcherID != c.dispatcherID || sip.Revision <= 0 || sip.SnapshotSHA256 == "" ||
sip.Artifact.CellID == "" || sip.Artifact.Revision <= 0 || sip.Artifact.ConfigSHA256 == "" {
if sip.Resource != resourceSIPConfig || sip.DispatcherID != c.dispatcherID || sip.Revision <= 0 {
return Snapshot{}, errors.New("SIP configuration identity or revision does not match the request")
}
seenTrunks := make(map[string]bool, len(sip.Trunks))
for _, trunk := range sip.Trunks {
if seenTrunks[trunk.TrunkID] {
return Snapshot{}, fmt.Errorf("SIP configuration repeats trunk %q", trunk.TrunkID)
}
seenTrunks[trunk.TrunkID] = true
seenCallers := make(map[string]bool, len(trunk.CallerProfiles))
for _, caller := range trunk.CallerProfiles {
if seenCallers[caller.CallerProfileID] {
return Snapshot{}, fmt.Errorf("SIP trunk %q repeats caller profile %q", trunk.TrunkID, caller.CallerProfileID)
}
seenCallers[caller.CallerProfileID] = true
}
}
taskPath := configReadPath + "/task/" + url.PathEscape(taskID)
taskBody, err := c.getConfig(ctx, taskPath)
@@ -165,10 +174,8 @@ func (c *Client) ReadTask(ctx context.Context, taskID, tenantID string) (Snapsho
return Snapshot{
TaskID: taskID, TenantID: tenantID, TenantKey: task.TenantKey,
SIPRevision: sip.Revision, SIPSnapshotSHA256: sip.SnapshotSHA256,
SIPCellID: sip.Artifact.CellID, SIPArtifactRevision: sip.Artifact.Revision,
SIPArtifactConfigSHA256: sip.Artifact.ConfigSHA256,
TaskRevision: task.TaskRevision, TaskStatus: task.Status,
SIPRevision: sip.Revision,
TaskRevision: task.TaskRevision, TaskStatus: task.Status,
TaskMaxConcurrentCalls: int64(task.MaxConcurrentCalls), TaskRingTimeoutMS: int64(task.RingTimeoutMS),
TaskMaxCallDurationMS: int64(task.MaxCallDurationMS), TaskRoutePolicyID: task.RoutePolicyID,
TaskCallerProfileID: task.CallerProfileID, TaskAllowedTrunkIDs: append([]string(nil), task.AllowedTrunkIDs...),
@@ -307,15 +314,15 @@ func responseErrorCode(body []byte) string {
}
type sipConfigResponse struct {
Resource string `json:"resource"`
DispatcherID string `json:"dispatcher_id"`
Revision int64 `json:"revision"`
SnapshotSHA256 string `json:"snapshot_sha256"`
Artifact struct {
CellID string `json:"cell_id"`
Revision int64 `json:"revision"`
ConfigSHA256 string `json:"config_sha256"`
} `json:"artifact"`
Resource string `json:"resource"`
DispatcherID string `json:"dispatcher_id"`
Revision int64 `json:"revision"`
Trunks []struct {
TrunkID string `json:"trunk_id"`
CallerProfiles []struct {
CallerProfileID string `json:"caller_profile_id"`
} `json:"caller_profiles"`
} `json:"trunks"`
}
type taskConfigResponse struct {
+33 -1
View File
@@ -67,6 +67,38 @@ func TestClientReadTaskRequestsAndValidatesThreeConfigEndpoints(t *testing.T) {
}
}
func TestClientReadTaskRejectsDuplicateSIPTrunksAndCallers(t *testing.T) {
for _, tc := range []struct {
name string
mutate func(map[string]any)
}{
{"duplicate trunk", func(sip map[string]any) {
trunks := sip["trunks"].([]any)
sip["trunks"] = append(trunks, trunks[0])
}},
{"duplicate caller", func(sip map[string]any) {
trunk := sip["trunks"].([]any)[0].(map[string]any)
profiles := trunk["caller_profiles"].([]any)
trunk["caller_profiles"] = append(profiles, profiles[0])
}},
} {
t.Run(tc.name, func(t *testing.T) {
fixtures := validConfigFixtures(t)
var sip map[string]any
if err := json.Unmarshal(fixtures[configReadPath+"/sip"], &sip); err != nil {
t.Fatal(err)
}
tc.mutate(sip)
fixtures[configReadPath+"/sip"] = marshalDiscoveryResponse(t, sip)
server := configFixtureServer(t, fixtures)
defer server.Close()
if _, err := newMockClient(t, server).ReadTask(context.Background(), mockTaskID, mockTenantID); err == nil {
t.Fatal("duplicate SIP configuration identity accepted")
}
})
}
}
func TestClientReadTaskRejectsSchemaInvalidResponse(t *testing.T) {
fixtures := validConfigFixtures(t)
fixtures[configReadPath+"/sip"] = readConfigFixture(t, "config-read-invalid-extra-property-v0.1.json")
@@ -139,7 +171,7 @@ func newMockClient(t *testing.T, server *httptest.Server) *Client {
func validConfigFixtures(t *testing.T) map[string][]byte {
t.Helper()
return map[string][]byte{
configReadPath + "/sip": readConfigFixture(t, "config-read-sip-v0.1.json"),
configReadPath + "/sip": readConfigFixture(t, "config-read-sip-v0.2.json"),
configReadPath + "/tasks": readConfigFixture(t, "task-discovery-snapshot-v0.2.json"),
configReadPath + "/task/" + mockTaskID: readConfigFixture(t, "config-read-task-v0.1.json"),
configReadPath + "/tenant/" + mockTenantID + "/quota": readConfigFixture(t, "config-read-tenant-quota-v0.1.json"),