feat: expose result subscription HTTP and WebSocket APIs

This commit is contained in:
2026-09-17 19:57:55 +08:00
parent 1c3300cc5b
commit 8485a662b2
38 changed files with 3462 additions and 26 deletions

View File

@@ -0,0 +1,141 @@
package httpserver
import (
"encoding/json"
"os"
"strings"
"testing"
"git.yuntuo.hk/k2safe/orbit/internal/testcontract"
"git.yuntuo.hk/k2safe/orbit/platform/events"
)
func TestEventFrameContract(t *testing.T) {
id := strings.Repeat("1", 32)
secret, _, err := events.NewSecret()
if err != nil {
t.Fatal(err)
}
valid := []string{
`{"protocol_version":"1","type":"events.hello","message_id":"` + id + `","body":{"subscription_id":"` + id + `","capacity":1}}`,
`{"protocol_version":"1","type":"events.ping","message_id":"` + id + `","session_id":"` + id + `","body":{}}`,
`{"protocol_version":"1","type":"event.ack","message_id":"` + id + `","session_id":"` + id + `","body":{"delivery_id":"` + id + `","epoch":"1","lease_secret":"` + secret + `"}}`,
}
for i, raw := range valid {
testcontract.EventFrame(t, "client", []byte(raw))
f, err := decodeEventRequest([]byte(raw), i == 0)
if err != nil {
t.Fatal(err)
}
switch i {
case 0:
var v eventHello
err = decodeEventJSON(f.Body, &v)
case 1:
var v struct{}
err = decodeEventJSON(f.Body, &v)
case 2:
var v events.AckInput
err = decodeEventJSON(f.Body, &v)
}
if err != nil {
t.Fatal(err)
}
}
for _, number := range []string{"0", "01", "18446744073709551616", "18446744073709551615"} {
raw := strings.Replace(valid[2], `"epoch":"1"`, `"epoch":"`+number+`"`, 1)
var frame eventFrame
_ = json.Unmarshal([]byte(raw), &frame)
var input events.AckInput
decodeErr := decodeEventJSON(frame.Body, &input)
var value any
_ = json.Unmarshal([]byte(raw), &value)
schemaErr := testcontract.EventSchema(t, "client").Validate(value)
wantValid := number == "18446744073709551615"
if (decodeErr == nil) != wantValid || (schemaErr == nil) != wantValid {
t.Fatal("event counter range disagrees with contract")
}
}
bad := []string{
strings.Replace(valid[0], `"body":`, `"session_id":null,"body":`, 1),
strings.Replace(valid[1], `"body":`, `"reply_to":"","body":`, 1),
strings.Replace(valid[1], `"body":`, `"error":null,"body":`, 1),
strings.Replace(valid[1], `"type":"events.ping"`, `"Type":"events.ping"`, 1),
strings.Replace(valid[1], `"protocol_version":"1"`, `"protocol_version":"1","protocol_version":"1"`, 1),
strings.Replace(valid[1], `"events.ping"`, `"worker.capacity"`, 1),
strings.Replace(valid[1], `"body":{}`, `"body":null`, 1),
}
for _, raw := range bad {
if _, err := decodeEventRequest([]byte(raw), strings.Contains(raw, "events.hello")); err == nil {
t.Fatal("accepted invalid event envelope")
}
}
// 标准Schema无法识别重复JSON键;实际解码器负责拒绝,其他方向约束由两者共同验证。
for _, raw := range bad[:4] {
var value any
if err := json.Unmarshal([]byte(raw), &value); err != nil {
t.Fatal(err)
}
if testcontract.EventSchema(t, "client").Validate(value) == nil {
t.Fatal("schema accepted direction or field violation")
}
}
}
func TestEventHTTPRequiredFieldsAndCounterCanonicalization(t *testing.T) {
id := strings.Repeat("1", 32)
for _, number := range []string{`"00"`, `"+1"`, `"01"`, `1`, `true`, `"18446744073709551616"`} {
var in events.CreateInput
raw := `{"name":"results","queue_id":"` + id + `","from_sequence":` + number + `}`
if decodeEventJSON([]byte(raw), &in) == nil {
t.Fatal("accepted invalid sequence")
}
}
for _, number := range []string{`"0"`, `null`, `"18446744073709551615"`} {
var in events.CreateInput
if err := decodeEventJSON([]byte(`{"name":"results","queue_id":"`+id+`","from_sequence":`+number+`}`), &in); err != nil {
t.Fatal(err)
}
}
for _, raw := range []string{`{"expected_revision":"1","reason":"pause"}`, `{"expected_revision":"01","reason":"pause","enabled":false}`, `{"expected_revision":"1","reason":"pause","enabled":null}`, `{"expected_revision":"1","reason":"pause","Enabled":false}`} {
var in events.ControlInput
if decodeEventJSON([]byte(raw), &in) == nil {
t.Fatal("accepted ambiguous control intent")
}
}
var good events.ControlInput
if err := decodeEventJSON([]byte(`{"expected_revision":"1","reason":"pause","enabled":false}`), &good); err != nil {
t.Fatal(err)
}
}
func TestEventDocumentationFramesMatchPublishedSchema(t *testing.T) {
raw, err := os.ReadFile("../../../docs/api/subscriptions.md")
if err != nil {
t.Fatal(err)
}
frames := 0
for _, block := range strings.Split(string(raw), "```json\n")[1:] {
snippet := strings.SplitN(block, "\n```", 2)[0]
var value map[string]json.RawMessage
if json.Unmarshal([]byte(snippet), &value) != nil {
t.Fatal("invalid documented JSON")
}
if value["protocol_version"] == nil {
continue
}
var kind string
if json.Unmarshal(value["type"], &kind) != nil {
t.Fatal("documented frame lacks type")
}
side := "server"
if kind == "events.hello" || kind == "events.ping" || kind == "event.ack" {
side = "client"
}
testcontract.EventFrame(t, side, []byte(snippet))
frames++
}
if frames != 6 {
t.Fatalf("expected six complete documented frames, got %d", frames)
}
}