This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
@@ -128,3 +129,98 @@ func TestUDLRecordGet_MissingCellHasNoValue(t *testing.T) {
|
||||
t.Fatalf("missing cell must omit `value`, got %s", body["value"])
|
||||
}
|
||||
}
|
||||
|
||||
// putRecords drives handleUDLRecordsPut with a raw JSON body and decodes the
|
||||
// per-record results.
|
||||
func putRecords(t *testing.T, s *Server, body string) (int, UDLPutBatchResponse) {
|
||||
t.Helper()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/udl/records", strings.NewReader(body))
|
||||
rr := httptest.NewRecorder()
|
||||
s.handleUDLRecordsPut(rr, req)
|
||||
|
||||
var out UDLPutBatchResponse
|
||||
if rr.Code == http.StatusOK {
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("decode response %q: %v", rr.Body.String(), err)
|
||||
}
|
||||
}
|
||||
return rr.Code, out
|
||||
}
|
||||
|
||||
// **The load-bearing property of the batch endpoint: one stale cell does not
|
||||
// fail the batch.**
|
||||
//
|
||||
// The single-record PUT answers a stale version with 409, which is right when
|
||||
// the caller wrote one thing. In a batch it would be wrong: bulk verbs ("mark
|
||||
// this whole series seen") routinely include episodes another device already
|
||||
// marked, and 409-ing there would abandon every cell after it. So staleness is
|
||||
// reported per record as accepted:false, with 200 on the request itself.
|
||||
func TestUDLRecordsPut_StaleCellDoesNotFailTheBatch(t *testing.T) {
|
||||
s := newUDLTestServer(t)
|
||||
|
||||
const uid = "zCarol"
|
||||
const cid = "bagdsaaaseries"
|
||||
// Seed s01e01 at version 4 — the batch below tries to write it at 2.
|
||||
if ok, err := s.storage.UDLUpsertIfNewer(uid, cid, "seen:s01e01", 4, 100, "b2xk", "", false, false); err != nil || !ok {
|
||||
t.Fatalf("seed: ok=%v err=%v", ok, err)
|
||||
}
|
||||
|
||||
code, out := putRecords(t, s, `{"records":[
|
||||
{"uid":"`+uid+`","cid":"`+cid+`","key":"seen:s01e01","version":2,"ts":200,"record":"c3RhbGU"},
|
||||
{"uid":"`+uid+`","cid":"`+cid+`","key":"seen:s01e02","version":1,"ts":200,"record":"ZnJlc2g"}
|
||||
]}`)
|
||||
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200 — a stale cell is an outcome, not an error", code)
|
||||
}
|
||||
if len(out.Results) != 2 {
|
||||
t.Fatalf("results = %d, want 2 (one per record, in order)", len(out.Results))
|
||||
}
|
||||
if out.Results[0].Accepted || out.Results[0].Error != "" {
|
||||
t.Fatalf("stale cell = %+v, want accepted:false with no error", out.Results[0])
|
||||
}
|
||||
if !out.Results[1].Accepted {
|
||||
t.Fatalf("fresh cell = %+v, want accepted:true", out.Results[1])
|
||||
}
|
||||
|
||||
// The fresh cell really landed, and the stale one did not overwrite.
|
||||
if _, body := getRecord(t, s, uid, cid, "seen:s01e02"); string(body["exists"]) != "true" {
|
||||
t.Fatalf("s01e02 was not written")
|
||||
}
|
||||
if _, body := getRecord(t, s, uid, cid, "seen:s01e01"); string(body["version"]) != "4" {
|
||||
t.Fatalf("s01e01 version = %s, want 4 (the stale write must not apply)", body["version"])
|
||||
}
|
||||
}
|
||||
|
||||
// A malformed record is reported in its own slot rather than rejecting the
|
||||
// request, for the same reason: the caller retries that cell, not the batch.
|
||||
func TestUDLRecordsPut_ReportsBadRecordPerSlot(t *testing.T) {
|
||||
s := newUDLTestServer(t)
|
||||
|
||||
code, out := putRecords(t, s, `{"records":[
|
||||
{"uid":"zDan","cid":"bagdsaaa","key":"","version":1,"ts":1,"record":"eA"},
|
||||
{"uid":"zDan","cid":"bagdsaaa","key":"seen:s01e01","version":1,"ts":1,"record":"eA"}
|
||||
]}`)
|
||||
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", code)
|
||||
}
|
||||
if out.Results[0].Error == "" {
|
||||
t.Fatalf("record with no key must report an error, got %+v", out.Results[0])
|
||||
}
|
||||
if !out.Results[1].Accepted {
|
||||
t.Fatalf("the valid record beside it must still land, got %+v", out.Results[1])
|
||||
}
|
||||
}
|
||||
|
||||
// An empty batch is a client bug, not a no-op: it almost always means the
|
||||
// caller built its record list wrong, and answering 200 with an empty array
|
||||
// would hide that.
|
||||
func TestUDLRecordsPut_RejectsEmptyBatch(t *testing.T) {
|
||||
s := newUDLTestServer(t)
|
||||
|
||||
if code, _ := putRecords(t, s, `{"records":[]}`); code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user