Skip to content

Commit b314502

Browse files
fix(cloud): accept server-canonical chunk hashes
1 parent cfe66b3 commit b314502

2 files changed

Lines changed: 54 additions & 9 deletions

File tree

internal/cloud/cloudserver/cloudserver.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,7 @@ func (s *CloudServer) handlePushChunk(w http.ResponseWriter, r *http.Request) {
458458
computedChunkID := chunkIDFromPayload(normalizedData)
459459
providedChunkID := strings.TrimSpace(req.ChunkID)
460460
if providedChunkID != "" && providedChunkID != computedChunkID {
461-
writeActionableError(w, http.StatusBadRequest, constants.UpgradeErrorClassRepairable, constants.UpgradeErrorCodePayloadInvalid, fmt.Sprintf("chunk_id does not match payload content hash (expected %s)", computedChunkID))
462-
return
461+
log.Printf("cloudserver: chunk_id mismatch for project %q: client=%q server=%q; accepting server-canonicalized payload", project, providedChunkID, computedChunkID)
463462
}
464463
clientCreatedAt := strings.TrimSpace(req.ClientCreatedAt)
465464
if clientCreatedAt != "" {

internal/cloud/cloudserver/cloudserver_test.go

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -646,13 +646,59 @@ func TestHandlerPushRejectsNormalizedEmptyProject(t *testing.T) {
646646
}
647647
}
648648

649-
func TestHandlerPushRejectsChunkIDPayloadMismatch(t *testing.T) {
650-
srv := New(&fakeStore{}, fakeAuth{}, 0)
651-
body := bytes.NewBufferString(`{"chunk_id":"deadbeef","project":"proj-a","created_by":"tester","data":{"sessions":[{"id":"s-1"}]}}`)
652-
rec := httptest.NewRecorder()
653-
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/sync/push", body))
654-
if rec.Code != http.StatusBadRequest {
655-
t.Fatalf("expected 400, got %d body=%q", rec.Code, rec.Body.String())
649+
func TestHandlerPushUsesServerHashWhenClientChunkIDMissingOrMismatched(t *testing.T) {
650+
payload := []byte(`{"sessions":[{"id":"s-1","directory":"/tmp/s-1"}]}`)
651+
normalizedPayload, err := coerceChunkProject(payload, "proj-a")
652+
if err != nil {
653+
t.Fatalf("coerce payload: %v", err)
654+
}
655+
wantChunkID := chunkIDFromPayload(normalizedPayload)
656+
tests := []struct {
657+
name string
658+
requestChunkID string
659+
forbiddenStored string
660+
}{
661+
{name: "mismatched chunk id", requestChunkID: "deadbeef", forbiddenStored: "deadbeef"},
662+
{name: "empty chunk id", requestChunkID: "", forbiddenStored: ""},
663+
}
664+
665+
for _, tt := range tests {
666+
t.Run(tt.name, func(t *testing.T) {
667+
st := &fakeStore{}
668+
srv := New(st, fakeAuth{}, 0)
669+
body := bytes.NewBufferString(`{"chunk_id":"` + tt.requestChunkID + `","project":"proj-a","created_by":"tester","data":` + string(payload) + `}`)
670+
671+
rec := httptest.NewRecorder()
672+
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/sync/push", body))
673+
if rec.Code != http.StatusOK {
674+
t.Fatalf("expected 200, got %d body=%q", rec.Code, rec.Body.String())
675+
}
676+
var response struct {
677+
ChunkID string `json:"chunk_id"`
678+
}
679+
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
680+
t.Fatalf("decode response: %v", err)
681+
}
682+
if response.ChunkID != wantChunkID {
683+
t.Fatalf("expected response chunk_id %q, got %q", wantChunkID, response.ChunkID)
684+
}
685+
if _, ok := st.chunks[wantChunkID]; !ok {
686+
t.Fatalf("expected store write under server chunk_id %q", wantChunkID)
687+
}
688+
for storedChunkID := range st.chunks {
689+
if storedChunkID == "" {
690+
t.Fatalf("expected stored chunk_id to be non-empty")
691+
}
692+
if storedChunkID != wantChunkID {
693+
t.Fatalf("expected stored chunk_id %q, got %q", wantChunkID, storedChunkID)
694+
}
695+
}
696+
if tt.forbiddenStored != "" {
697+
if _, ok := st.chunks[tt.forbiddenStored]; ok {
698+
t.Fatalf("expected client chunk_id %q not to be used for storage", tt.forbiddenStored)
699+
}
700+
}
701+
})
656702
}
657703
}
658704

0 commit comments

Comments
 (0)