Skip to content
quarry Artifact [ae5cac309f]
Login

Artifact [ae5cac309f]

Artifact ae5cac309f1d864e5b6f82649e450903844e4487bcd33b2285a3777bcac6e9ef:


package oltp

import "testing"

func str(s string) *string { return &s }
func flag(b bool) *bool    { return &b }

// A patch writes the fields it names, leaves the rest alone, and
// records one row per field that actually changed -- with the before
// value read inside the same transaction as the write.
func TestPatchRepoWritesAndRecordsOnlyWhatChanged(t *testing.T) {
	db := testDB(t)
	if err := db.AddRepo("proj", t.TempDir(), "the original", Public); err != nil {
		t.Fatal(err)
	}
	if err := db.SetRepoTopics("proj", "forge zit"); err != nil {
		t.Fatal(err)
	}

	changes, err := db.PatchRepo("proj", RepoPatch{Description: str("rewritten")}, "dmestas")
	if err != nil {
		t.Fatal(err)
	}
	if len(changes) != 1 {
		t.Fatalf("want one recorded change, got %d: %+v", len(changes), changes)
	}
	c := changes[0]
	if c.Field != "description" || c.Was != "the original" || c.Now != "rewritten" {
		t.Errorf("change = %+v, want description the original -> rewritten", c)
	}
	if c.Actor != "dmestas" {
		t.Errorf("actor = %q, want dmestas: the log cannot answer WHO", c.Actor)
	}

	r, ok, err := db.RepoByName("proj")
	if err != nil || !ok {
		t.Fatalf("RepoByName: ok=%v err=%v", ok, err)
	}
	if r.Description != "rewritten" {
		t.Errorf("description = %q: the named field was not written", r.Description)
	}
	if r.Topics != "forge zit" {
		t.Errorf("topics = %q: a description-only patch rewrote another column", r.Topics)
	}

	logged, err := db.SettingsLog("proj", 10)
	if err != nil {
		t.Fatal(err)
	}
	if len(logged) != 1 || logged[0].Field != "description" || logged[0].Was != "the original" {
		t.Errorf("SettingsLog = %+v, want the one description change", logged)
	}
	if logged[0].At == "" {
		t.Error("the log row carries no timestamp, so it cannot be lined up against anything")
	}
}

// Setting a field to the value it already holds is not a change: no
// write, no log row. Only the transaction can know that.
func TestPatchRepoIgnoresANoOp(t *testing.T) {
	db := testDB(t)
	if err := db.AddRepo("proj", t.TempDir(), "same", Public); err != nil {
		t.Fatal(err)
	}
	changes, err := db.PatchRepo("proj", RepoPatch{Description: str("same")}, "dmestas")
	if err != nil {
		t.Fatalf("a no-op patch must not be an error -- it arrived and changed nothing: %v", err)
	}
	if len(changes) != 0 {
		t.Errorf("a no-op patch recorded %+v", changes)
	}
	logged, _ := db.SettingsLog("proj", 10)
	if len(logged) != 0 {
		t.Errorf("a no-op patch wrote an audit row: %+v", logged)
	}
}

// Mirrorable is skipped for the WRITE while private and never for the
// read. The comparison is against the STORED flag: comparing against
// the effective answer is what laundered "false because private" into
// "false because asked" and left a public repository unclonable.
func TestPatchRepoSkipsMirrorableWhilePrivate(t *testing.T) {
	db := testDB(t)
	if err := db.AddRepo("vault", t.TempDir(), "", Private); err != nil {
		t.Fatal(err)
	}
	changes, err := db.PatchRepo("vault",
		RepoPatch{Mirrorable: flag(false), Description: str("still writable")}, "dmestas")
	if err != nil {
		t.Fatal(err)
	}
	for _, c := range changes {
		if c.Field == "mirrorable" {
			t.Error("mirrorable was written while the repository is private: a disabled " +
				"checkbox's value is not the admin's intent")
		}
	}
	r, _, _ := db.RepoByName("vault")
	if !r.Mirrorable {
		t.Error("the stored mirrorable flag was zeroed during a private detour, so the " +
			"repository comes back public and unclonable")
	}
	if r.Description != "still writable" {
		t.Errorf("description = %q: skipping mirrorable also skipped the rest of the patch",
			r.Description)
	}
}

// A patch against a repository with no row is refused, not reported as
// a successful no-op.
func TestPatchRepoRefusesAnUnknownRepository(t *testing.T) {
	db := testDB(t)
	if _, err := db.PatchRepo("ghost", RepoPatch{Description: str("x")}, "dmestas"); err == nil {
		t.Error("a patch against an unregistered repository reported success")
	}
}

// Taking a repository private still carries the RFC 0004 category
// strings with it -- that half writes a different table and runs after
// the commit, and dropping it would leave a private repository readable.
func TestPatchRepoCarriesVisibilityCategories(t *testing.T) {
	db := testDB(t)
	if err := db.AddRepo("proj", t.TempDir(), "", Public); err != nil {
		t.Fatal(err)
	}
	if _, err := db.PatchRepo("proj", RepoPatch{Visibility: str(Private)}, "dmestas"); err != nil {
		t.Fatal(err)
	}
	if got := db.VisibilityName("proj"); got != "Private" {
		t.Errorf("visibility name = %q, want Private: the category half of the change was "+
			"dropped, so the repository reads private but is not", got)
	}
}

// A value longer than a truncated timestamp survives whole.
//
// This is not a hypothetical bound. The recorded values were being cut
// at 19 characters by oltp.clip -- the helper that shortens a timestamp
// to seconds -- because it has the same signature as the one meant for
// this and a rename missed the call site. Both sides were cut the same
// way, so a real change was stored as was == now: the log denied the
// change it existed to record, and did it while reporting success.
//
// The values here are deliberately longer than 19 characters and differ
// only after that point, which is the case every shorter fixture missed.
func TestPatchRepoKeepsLongValuesWhole(t *testing.T) {
	db := testDB(t)
	const before = "dantest e2e sandbox (Jan)"
	const after = "dantest e2e sandbox (audit probe)"
	if err := db.AddRepo("proj", t.TempDir(), before, Public); err != nil {
		t.Fatal(err)
	}
	changes, err := db.PatchRepo("proj", RepoPatch{Description: str(after)}, "dmestas")
	if err != nil {
		t.Fatal(err)
	}
	if len(changes) != 1 {
		t.Fatalf("want one change, got %+v", changes)
	}
	c := changes[0]
	if c.Was != before {
		t.Errorf("was = %q (%d chars), want %q (%d): the previous value is being truncated",
			c.Was, len(c.Was), before, len(before))
	}
	if c.Now != after {
		t.Errorf("now = %q (%d chars), want %q (%d): the new value is being truncated",
			c.Now, len(c.Now), after, len(after))
	}
	if c.Was == c.Now {
		t.Error("a real change was recorded as was == now, so the log denies the change " +
			"it is recording")
	}
	logged, err := db.SettingsLog("proj", 10)
	if err != nil {
		t.Fatal(err)
	}
	if len(logged) != 1 || logged[0].Was != before || logged[0].Now != after {
		t.Errorf("read back = %+v, want %q -> %q", logged, before, after)
	}
}