feat: add idempotent batch task submission and recovery examples
This commit is contained in:
79
internal/transport/httpserver/batch_test.go
Normal file
79
internal/transport/httpserver/batch_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,17 @@ import (
|
||||
type endpoint func(*http.Request, auth.Token) (any, int, error)
|
||||
|
||||
func registerTasks(mux *http.ServeMux, s *service.Service) {
|
||||
mux.HandleFunc("POST /v1/tasks:batch", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
|
||||
if len(r.Header.Values("Idempotency-Key")) != 0 {
|
||||
return nil, 0, fault.Invalid("batch requires per-item idempotency_key, not an Idempotency-Key header")
|
||||
}
|
||||
var in service.BatchSubmitInput
|
||||
if err := decodeBody(r, &in); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out, err := s.SubmitBatch(r.Context(), t, in)
|
||||
return out, http.StatusOK, err
|
||||
}))
|
||||
mux.HandleFunc("POST /v1/tasks", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
|
||||
var in service.SubmitInput
|
||||
if err := decodeBody(r, &in); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user