feat: add idempotent batch task submission and recovery examples

This commit is contained in:
2026-09-17 21:21:18 +08:00
parent a8a8dcfe1e
commit 571107f96d
27 changed files with 1375 additions and 24 deletions

View File

@@ -0,0 +1,79 @@
package httpserver
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
)
type batchAuthStore struct {
service.Store
authCalls int
}
func batchHTTPToken(t *testing.T) string {
t.Helper()
token, _, err := auth.Issue(core.ID(strings.Repeat("1", 32)), core.ID(strings.Repeat("2", 32)))
if err != nil {
t.Fatal(err)
}
return "Bearer " + token
}
func (s *batchAuthStore) AuthorizeSubmit(context.Context, auth.Token) error {
s.authCalls++
return fault.Unauthorized
}
func TestBatchHTTPRejectsWholeMalformedEnvelopeBeforeWrites(t *testing.T) {
for _, test := range []struct{ body, key, code string }{
{`{"items":[{"idempotency_key":"a","task":{}},{"idempotency_key":"b","task":{"unexpected":1}}]}`, "", "INVALID_ARGUMENT"},
{`{"items":[{"idempotency_key":"a","task":{"retry":{"MaxAttempts":1}}}]}`, "", "INVALID_ARGUMENT"},
{`{"items":[{"idempotency_key":"a","task":{"payload":{"x":1,"x":2}}}]}`, "", "INVALID_ARGUMENT"},
{`{"items":[null]}`, "", "INVALID_ARGUMENT"},
{`{"items":[{"idempotency_key":"a","task":null}]}`, "", "INVALID_ARGUMENT"},
{`{"items":[{"idempotency_key":"a","task":{}},{"idempotency_key":"a","task":{}}]}`, "", "INVALID_ARGUMENT"},
{`{"items":[]}`, "", "INVALID_ARGUMENT"},
{`{"items":[{"idempotency_key":"a","task":{}}]}`, "whole-batch", "INVALID_ARGUMENT"},
{`{"items":[{"idempotency_key":"a","task":{"payload":"` + strings.Repeat("x", 512<<10) + `"}}]}`, "", "BODY_TOO_LARGE"},
} {
s := &batchAuthStore{}
h := New("", nil, service.New(s)).Handler
r := httptest.NewRequest("POST", "/v1/tasks:batch", strings.NewReader(test.body))
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", batchHTTPToken(t))
if test.key != "" {
r.Header.Set("Idempotency-Key", test.key)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
var got struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil || got.Error.Code != test.code || s.authCalls != 0 {
t.Fatalf("got=%s auth=%d want=%s", w.Body.String(), s.authCalls, test.code)
}
}
}
func TestBatchHTTPAuthenticatesAllInvalidItems(t *testing.T) {
s := &batchAuthStore{}
r := httptest.NewRequest("POST", "/v1/tasks:batch", strings.NewReader(`{"items":[{"idempotency_key":"a","task":{}}]}`))
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", batchHTTPToken(t))
w := httptest.NewRecorder()
New("", nil, service.New(s)).Handler.ServeHTTP(w, r)
if w.Code != http.StatusUnauthorized || s.authCalls != 1 {
t.Fatal(w.Body.String(), s.authCalls)
}
}