Skip to content
quarry Artifact [bda26c7865]
Login

Artifact [bda26c7865]

Artifact bda26c78656372aa8a8b15cf3c79dd31afa1006de84fb3b1165b20a134bce60b:


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)
	}
}