package api
import (
"context"
"testing"
"craftdesign.group/quarry/internal/ops"
)
// updateOnly invokes repo.update with one prepared body and hands back
// what the registry was asked to do.
func updateOnly(t *testing.T, in repoUpdateIn) *fakeRegistry {
t.Helper()
reg := &fakeRegistry{}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v",
Repos: NewRepos(map[string]*Repo{"widgets": {}}), Registry: reg}); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(context.Background(), "repo.update", in); err != nil {
t.Fatal(err)
}
return reg
}
// A partial update must carry ONLY the fields its body named.
//
// This is the property that makes the lost update impossible. While the
// handler merged the named fields over a read of the whole settings
// row, every update wrote every column, so two admins saving different
// panels in the same moment each wrote back their own stale copy of the
// other's field and the later save reverted the earlier one -- no
// error, no conflict, no trace. Two writes that touch disjoint columns
// cannot do that to each other.
//
// Asserted on the patch the registry RECEIVES rather than on the
// handler's source: the defect was about which columns a write reaches,
// and that is visible only in what crosses the seam.
func TestPartialUpdateCarriesOnlyNamedFields(t *testing.T) {
desc := "just the description"
reg := updateOnly(t, repoUpdateIn{Name: "widgets", Description: &desc})
if len(reg.updates) != 1 {
t.Fatalf("want exactly one write, got %d: %+v", len(reg.updates), reg.updates)
}
got := reg.updates[0]
if got.Description == nil || *got.Description != desc {
t.Errorf("the named field did not reach the registry: %+v", got)
}
for _, f := range []struct {
name string
set bool
}{
{"Visibility", got.Visibility != nil},
{"Homepage", got.Homepage != nil},
{"Topics", got.Topics != nil},
{"Mirrorable", got.Mirrorable != nil},
} {
if f.set {
t.Errorf("a description-only update also wrote %s: a concurrent change to that "+
"field is silently reverted, and a wrong read of it becomes durable", f.name)
}
}
if len(reg.chats) != 0 {
t.Errorf("a description-only update also wrote the chat setting: %v", reg.chats)
}
}
// Chat has its own write path, and it must be just as conditional: it
// was called with the merged value on every update, so a body that
// never mentioned chat still rewrote it.
func TestChatOnlyUpdateTouchesNothingElse(t *testing.T) {
on := true
reg := updateOnly(t, repoUpdateIn{Name: "widgets", Chat: &on})
if len(reg.chats) != 1 || !reg.chats[0] {
t.Errorf("the chat setting was not written: %v", reg.chats)
}
if len(reg.updates) != 0 {
t.Errorf("a chat-only update also wrote the settings row: %+v", reg.updates)
}
}
// A body that names no setting at all writes nothing, rather than
// writing every column back as it was read.
func TestUpdateNamingNothingWritesNothing(t *testing.T) {
reg := updateOnly(t, repoUpdateIn{Name: "widgets"})
if len(reg.updates) != 0 || len(reg.chats) != 0 {
t.Errorf("an empty update still wrote: settings=%+v chat=%v", reg.updates, reg.chats)
}
}