258 lines
8.6 KiB
Go
258 lines
8.6 KiB
Go
package contracts_test
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/tenant"
|
|
)
|
|
|
|
const v1Dir = "upstream/v1"
|
|
const v1Base = "https://go-sip.local/contracts/v1/"
|
|
|
|
type offlineLoader struct{}
|
|
|
|
func (offlineLoader) Load(url string) (any, error) {
|
|
return nil, fmt.Errorf("schema is not in the immutable local bundle: %s", url)
|
|
}
|
|
|
|
func compileV1(t *testing.T, name string) *jsonschema.Schema {
|
|
t.Helper()
|
|
compiler := jsonschema.NewCompiler()
|
|
compiler.AssertFormat()
|
|
compiler.UseLoader(offlineLoader{})
|
|
paths, err := filepath.Glob(filepath.Join(v1Dir, "*.schema.json"))
|
|
if err != nil || len(paths) == 0 {
|
|
t.Fatalf("contract bundle missing: %v", err)
|
|
}
|
|
for _, path := range paths {
|
|
var schema any
|
|
if err := json.Unmarshal(readV1(t, path), &schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := compiler.AddResource(v1Base+filepath.Base(path), schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
schema, err := compiler.Compile(v1Base + name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return schema
|
|
}
|
|
|
|
func readV1(t *testing.T, path string) []byte {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func objectV1(t *testing.T, path string) map[string]any {
|
|
t.Helper()
|
|
var value map[string]any
|
|
if err := json.Unmarshal(readV1(t, path), &value); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func TestV1BundleFixtures(t *testing.T) {
|
|
for _, name := range []string{"mq.schema.json", "event-payloads.schema.json", "dispatcher-config.schema.json"} {
|
|
t.Run(name, func(t *testing.T) { compileV1(t, name) })
|
|
}
|
|
var cases []struct {
|
|
File string `json:"file"`
|
|
Schema string `json:"schema"`
|
|
Valid bool `json:"valid"`
|
|
}
|
|
if err := json.Unmarshal(readV1(t, filepath.Join(v1Dir, "fixtures.json")), &cases); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(cases) < 16 {
|
|
t.Fatal("missing bounded positive and negative message/config fixtures")
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.File, func(t *testing.T) {
|
|
schema := compileV1(t, tc.Schema)
|
|
value := objectV1(t, filepath.Join(v1Dir, tc.File))
|
|
err := schema.Validate(value)
|
|
if (err == nil) != tc.Valid {
|
|
t.Fatalf("valid=%v, validation=%v", tc.Valid, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestV1ExecuteIdentityAndTenantConstraints(t *testing.T) {
|
|
schema := compileV1(t, "mq.schema.json")
|
|
for _, tc := range []struct {
|
|
name string
|
|
edit func(map[string]any)
|
|
}{
|
|
{"missing dispatcher", func(m map[string]any) { delete(m, "dispatcher_id") }},
|
|
{"non UUID dispatcher", func(m map[string]any) { m["dispatcher_id"] = "dispatcher-a" }},
|
|
{"uppercase dispatcher", func(m map[string]any) { m["dispatcher_id"] = strings.ToUpper(m["dispatcher_id"].(string)) }},
|
|
{"wildcard tenant", func(m map[string]any) { m["tenant_key"] = "tenant.#" }},
|
|
{"old version", func(m map[string]any) { m["schema_version"] = "1.0" }},
|
|
{"extra root", func(m map[string]any) { m["metadata"] = map[string]any{} }},
|
|
{"extra payload", func(m map[string]any) { m["payload"].(map[string]any)["mode"] = "full_ai" }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
value := objectV1(t, filepath.Join(v1Dir, "examples/call-execute.json"))
|
|
tc.edit(value)
|
|
if err := schema.Validate(value); err == nil {
|
|
t.Fatal("invalid envelope accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestV1ResponseCorrelationAndResultBranches(t *testing.T) {
|
|
schema := compileV1(t, "mq.schema.json")
|
|
for _, tc := range []struct {
|
|
name string
|
|
edit func(map[string]any)
|
|
}{
|
|
{"missing correlation", func(m map[string]any) { delete(m, "correlation_id") }},
|
|
{"wrong result", func(m map[string]any) { m["status"] = "rejected"; m["reason_code"] = "ok" }},
|
|
{"response deadline", func(m map[string]any) { m["not_after"] = "2026-09-21T00:00:30Z" }},
|
|
{"missing command ID", func(m map[string]any) { delete(m["payload"].(map[string]any), "command_id") }},
|
|
{"extra result", func(m map[string]any) { m["payload"].(map[string]any)["raw"] = map[string]any{} }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
value := objectV1(t, filepath.Join(v1Dir, "examples/command-query-result.json"))
|
|
tc.edit(value)
|
|
if err := schema.Validate(value); err == nil {
|
|
t.Fatal("invalid query response accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestV1UploadIsAFactNotASaaSHandshake(t *testing.T) {
|
|
schema := compileV1(t, "mq.schema.json")
|
|
for _, field := range []string{"oss_id", "verified", "token", "upload_url", "access_key_secret"} {
|
|
t.Run(field, func(t *testing.T) {
|
|
value := objectV1(t, filepath.Join(v1Dir, "examples/event-recording-uploaded.json"))
|
|
value["payload"].(map[string]any)[field] = "not-a-real-value"
|
|
if err := schema.Validate(value); err == nil {
|
|
t.Fatal("uploaded fact accepted a credential or SaaS processing claim")
|
|
}
|
|
})
|
|
}
|
|
for _, kind := range []string{"recording.upload-session.request", "recording.upload-session.result", "recording.complete", "recording.verified"} {
|
|
if strings.Contains(string(readV1(t, filepath.Join(v1Dir, "mq.schema.json"))), `"`+kind+`"`) {
|
|
t.Errorf("removed upload handshake is still in the published schema: %s", kind)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestV1DispatcherConfigDoesNotAcceptSecretsOrTTLOverride(t *testing.T) {
|
|
schema := compileV1(t, "dispatcher-config.schema.json")
|
|
for _, field := range []string{"access_key_id", "access_key_secret", "grant_ttl_seconds", "token"} {
|
|
t.Run(field, func(t *testing.T) {
|
|
value := objectV1(t, filepath.Join(v1Dir, "examples/dispatcher-config.json"))
|
|
value["oss"].(map[string]any)[field] = "not-a-real-value"
|
|
if err := schema.Validate(value); err == nil {
|
|
t.Fatal("unexpected secret/override field accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestV1TopologyMatchesDispatcherRoutes(t *testing.T) {
|
|
var topology struct {
|
|
Exchanges map[string]struct {
|
|
Type string `json:"type"`
|
|
Durable bool `json:"durable"`
|
|
} `json:"exchanges"`
|
|
Inbox string `json:"inbox_queue"`
|
|
Dead string `json:"dead_letter_queue"`
|
|
Inbound string `json:"inbound_key"`
|
|
Outbound string `json:"outbound_key"`
|
|
Durable bool `json:"business_queues_durable"`
|
|
Persistent bool `json:"message_persistent"`
|
|
Mandatory bool `json:"publish_mandatory"`
|
|
Confirm bool `json:"publisher_confirms"`
|
|
TenantBytes int `json:"tenant_key_max_utf8_bytes"`
|
|
TokenSeconds int `json:"upload_token_seconds"`
|
|
}
|
|
if err := json.Unmarshal(readV1(t, filepath.Join(v1Dir, "mq-topology.json")), &topology); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !topology.Durable || !topology.Persistent || !topology.Mandatory || !topology.Confirm || topology.TenantBytes != 196 || topology.TokenSeconds != 900 {
|
|
t.Fatal("topology weakens the approved queue/token boundary")
|
|
}
|
|
if len(topology.Exchanges) != 3 {
|
|
t.Fatal("unexpected exchange topology")
|
|
}
|
|
for _, name := range []string{"agent-call.dispatchers.v2", "agent-call.saas.v2", "agent-call.dead-letter.v2"} {
|
|
x, ok := topology.Exchanges[name]
|
|
if !ok || x.Type != "topic" || !x.Durable {
|
|
t.Fatalf("wrong exchange contract: %s", name)
|
|
}
|
|
}
|
|
id := "c046b893-8628-4589-ae50-619d049248a6"
|
|
for _, key := range []string{"tenant-a", "租户.甲", strings.Repeat("a", 196)} {
|
|
route, err := tenant.NewDispatcherRoute(id, key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := strings.NewReplacer("<dispatcher_id>", id, "<tenant_key>", key)
|
|
if route.InboxQueue != r.Replace(topology.Inbox) || route.DeadLetterQueue != r.Replace(topology.Dead) || route.InboundKey != r.Replace(topology.Inbound) || route.OutboundKey != r.Replace(topology.Outbound) {
|
|
t.Fatal("implementation differs from the published route templates")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestV1ManifestPinsEveryBundleFile(t *testing.T) {
|
|
var manifest struct {
|
|
Version string `json:"version"`
|
|
Source string `json:"source"`
|
|
Files map[string]string `json:"files"`
|
|
}
|
|
if err := json.Unmarshal(readV1(t, filepath.Join(v1Dir, "manifest.json")), &manifest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if manifest.Version != "v1" || manifest.Source != "project-development-v1" || len(manifest.Files) == 0 {
|
|
t.Fatal("invalid project-local provenance")
|
|
}
|
|
err := filepath.WalkDir(v1Dir, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil || entry.IsDir() {
|
|
return err
|
|
}
|
|
rel, err := filepath.Rel(v1Dir, path)
|
|
if err != nil || rel == "manifest.json" {
|
|
return err
|
|
}
|
|
actual := sha256.Sum256(readV1(t, path))
|
|
if manifest.Files[filepath.ToSlash(rel)] != hex.EncodeToString(actual[:]) {
|
|
t.Errorf("missing/incorrect manifest hash: %s", rel)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for rel := range manifest.Files {
|
|
if !filepath.IsLocal(rel) {
|
|
t.Errorf("non-local manifest entry: %s", rel)
|
|
continue
|
|
}
|
|
if _, err := os.Stat(filepath.Join(v1Dir, rel)); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
}
|