feat: expose result subscription HTTP and WebSocket APIs
This commit is contained in:
141
internal/transport/httpserver/event_codec_test.go
Normal file
141
internal/transport/httpserver/event_codec_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user