fix(api): bound claim wire fields end to end

This commit is contained in:
QiuSW
2026-08-05 01:25:23 +08:00
parent 57a5b2e91c
commit 7ea1c5349f
17 changed files with 514 additions and 98 deletions
+14 -2
View File
@@ -14,7 +14,10 @@ import (
"github.com/gin-gonic/gin"
)
const maxClaimJSONBytes = 4096
const (
maxClaimJSONBytes = 4096
maxClaimResponseJSONBytes = 32 * 1024
)
func claimNext(options Options) gin.HandlerFunc {
return func(context *gin.Context) {
@@ -35,7 +38,16 @@ func claimNext(options Options) gin.HandlerFunc {
context.Status(http.StatusNoContent)
return
}
context.JSON(http.StatusOK, response)
if !taskclaim.ValidClaimResponse(response) {
context.Status(http.StatusServiceUnavailable)
return
}
encoded, err := json.Marshal(response)
if err != nil || len(encoded) > maxClaimResponseJSONBytes {
context.Status(http.StatusServiceUnavailable)
return
}
context.Data(http.StatusOK, "application/json; charset=utf-8", encoded)
}
}
+47
View File
@@ -3,6 +3,7 @@ package server_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
@@ -98,6 +99,52 @@ func TestClaimNextStrictJSONSuccessEmptyAndErrors(t *testing.T) {
}
}
func TestClaimResponseWorstLegalFieldsStayBelowCapAndInvalidServiceOutputFailsClosed(t *testing.T) {
goodsID := strings.Repeat("1", 32)
worst := taskclaim.ClaimResponse{
Task: taskclaim.ClaimedTask{
ID: claimTaskID, Version: 3, Title: strings.Repeat("<", 120),
ProductURL: "https://mobile.yangkeduo.com/goods.html?goods_id=" + goodsID,
GoodsID: goodsID, SKUColor: strings.Repeat("<", 80), SKUSize: strings.Repeat("<", 80),
Quantity: 9_223_372_036_854_775_807, MaxTotalPrice: strings.Repeat("9", 29) + ".00",
},
Authorization: taskclaim.ClaimedAuthorization{ID: "70000000-0000-4000-8000-000000000001", TaskVersion: 2, ExpiresAt: "9999-12-31T23:59:59.999999999Z"},
Attempt: taskclaim.ClaimedAttempt{ID: claimAttemptID, ClaimToken: strings.Repeat("a", 64), ClaimGeneration: 9_223_372_036_854_775_807, LeaseExpiresAt: "9999-12-31T23:59:59.999999999Z"},
}
authenticator := &fakeDeviceAuthenticator{principal: deviceauth.Principal{ID: claimDeviceID}}
service := &fakeTaskClaimService{claimResponse: worst, claimFound: true}
router, _ := newRouterWithClaimService(t, &memoryStore{}, emptyDetailStore{}, emptyEvidenceStore{}, authenticator, service)
request := `{"session_id":"` + claimSessionID + `","claim_request_id":"` + claimRequestID + `"}`
response := serveClaimJSON(router, "/api/v1/tasks/claim-next", request, "application/json")
if response.Code != http.StatusOK || !json.Valid(response.Body.Bytes()) || response.Body.Len() >= 32*1024 {
t.Fatalf("worst legal response = status %d, bytes %d, valid JSON %v", response.Code, response.Body.Len(), json.Valid(response.Body.Bytes()))
}
mutations := map[string]func(*taskclaim.ClaimResponse){
"invalid utf8 title": func(response *taskclaim.ClaimResponse) { response.Task.Title = string([]byte{0xff}) },
"overlong title": func(response *taskclaim.ClaimResponse) { response.Task.Title += "<" },
"overlong goods id": func(response *taskclaim.ClaimResponse) {
response.Task.GoodsID += "1"
response.Task.ProductURL += "1"
},
"overlong color": func(response *taskclaim.ClaimResponse) { response.Task.SKUColor += "<" },
"overlong size": func(response *taskclaim.ClaimResponse) { response.Task.SKUSize += "<" },
"overlong money": func(response *taskclaim.ClaimResponse) { response.Task.MaxTotalPrice = strings.Repeat("9", 30) + ".00" },
}
for name, mutate := range mutations {
t.Run(name, func(t *testing.T) {
invalid := worst
mutate(&invalid)
service := &fakeTaskClaimService{claimResponse: invalid, claimFound: true}
router, _ := newRouterWithClaimService(t, &memoryStore{}, emptyDetailStore{}, emptyEvidenceStore{}, authenticator, service)
response := serveClaimJSON(router, "/api/v1/tasks/claim-next", request, "application/json")
if response.Code != http.StatusServiceUnavailable || response.Body.Len() != 0 {
t.Fatalf("invalid service response = %d %q", response.Code, response.Body.String())
}
})
}
}
func TestRenewStrictBindingResponseAndFixedErrors(t *testing.T) {
authenticator := &fakeDeviceAuthenticator{principal: deviceauth.Principal{ID: claimDeviceID}}
service := &fakeTaskClaimService{renewResponse: taskclaim.RenewResponse{TaskID: claimTaskID, AttemptID: claimAttemptID, ClaimGeneration: 1, LeaseExpiresAt: "2026-08-04T01:04:00Z"}}