Skip to content
quarry Artifact [6b0415985e]
Login

Artifact [6b0415985e]

Artifact 6b0415985e243843be086a0e2e7e76fad0c067f17a6fa5009b95955718d277f0:


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},
		{"Chat", got.Chat != 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)
		}
	}
}

// Chat travels in the same patch as everything else, and carries
// nothing with it.
//
// It used to be a second write path called with the merged value on
// every update, so a body that never mentioned chat still rewrote it.
// Folding it in means one write, one actor and one audit record for a
// save; two paths would each need their own and could then disagree
// about what a single save did.
func TestChatOnlyUpdateTouchesNothingElse(t *testing.T) {
	on := true
	reg := updateOnly(t, repoUpdateIn{Name: "widgets", Chat: &on})

	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.Chat == nil || !*got.Chat {
		t.Errorf("the chat setting did not reach the registry: %+v", got)
	}
	for _, f := range []struct {
		name string
		set  bool
	}{
		{"Description", got.Description != nil},
		{"Visibility", got.Visibility != nil},
		{"Homepage", got.Homepage != nil},
		{"Topics", got.Topics != nil},
		{"Mirrorable", got.Mirrorable != nil},
	} {
		if f.set {
			t.Errorf("a chat-only update also wrote %s", f.name)
		}
	}
}

// Every settings write is attributed. An audit trail that cannot say
// who is most of the way to no audit trail at all: the incident that
// prompted this one was unattributable for an hour because the only
// record said "settings changed" and nothing else.
func TestUpdateCarriesTheActor(t *testing.T) {
	desc := "x"
	reg := updateOnly(t, repoUpdateIn{Name: "widgets", Description: &desc})
	if len(reg.actors) != 1 {
		t.Fatalf("want one attributed write, got %v", reg.actors)
	}
}

// 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 {
		t.Errorf("an empty update still wrote: %+v", reg.updates)
	}
}