Skip to content
quarry Artifact [fac228bbc8]
Login

Artifact [fac228bbc8]

Artifact fac228bbc87f9a8f0fec7bfd031cdf0aded61df840a55bd85caf3393f8460f43:


package oltp

import (
	"fmt"
	"strings"
	"testing"
)

// purgeKeeps names the repo-keyed tables PurgeRepo deliberately leaves,
// each with the reason. Everything else with a `repo` column must be
// empty for a purged name, so a table added later without a purge line
// fails this test instead of leaking into the next repository to take
// the name (quarry#294).
var purgeKeeps = map[string]string{
	// The forge's own error log: read by site admins on the ops page,
	// never through a repository, and the rows are about the code path
	// that failed rather than anything the repository owned.
	"exception": "site-level error log",
}

// seedRepoRow inserts one row naming repo into table, filling every
// column with a value of its declared type. n keeps text and integer
// keys distinct across calls; other integers are 1, which every CHECK
// in the schema admits.
func seedRepoRow(t *testing.T, db *DB, table, repo string, n int) {
	t.Helper()
	rows, err := db.db.Query(`SELECT name, type, pk FROM pragma_table_info(?)`, table)
	if err != nil {
		t.Fatal(err)
	}
	var cols []string
	var args []any
	for rows.Next() {
		var name, typ string
		var pk int
		if err := rows.Scan(&name, &typ, &pk); err != nil {
			t.Fatal(err)
		}
		cols = append(cols, name)
		switch {
		case name == "repo":
			args = append(args, repo)
		case strings.Contains(strings.ToUpper(typ), "INT") && pk > 0:
			args = append(args, n)
		case strings.Contains(strings.ToUpper(typ), "INT"):
			args = append(args, 1)
		default:
			args = append(args, fmt.Sprintf("%s-%s-%d", table, name, n))
		}
	}
	if err := rows.Err(); err != nil {
		t.Fatal(err)
	}
	q := fmt.Sprintf(`INSERT INTO %s (%s) VALUES (%s)`, table,
		strings.Join(cols, ", "), strings.TrimSuffix(strings.Repeat("?, ", len(cols)), ", "))
	if _, err := db.w.Exec(q, args...); err != nil {
		t.Fatalf("seed %s: %v", table, err)
	}
}

// repoKeyedTables is every table with a column named repo.
func repoKeyedTables(t *testing.T, db *DB) []string {
	t.Helper()
	rows, err := db.db.Query(`SELECT m.name FROM sqlite_master m, pragma_table_info(m.name) c
		WHERE m.type = 'table' AND c.name = 'repo' ORDER BY m.name`)
	if err != nil {
		t.Fatal(err)
	}
	defer rows.Close()
	var out []string
	for rows.Next() {
		var n string
		if err := rows.Scan(&n); err != nil {
			t.Fatal(err)
		}
		out = append(out, n)
	}
	return out
}

func countWhere(t *testing.T, db *DB, q string, args ...any) int {
	t.Helper()
	var n int
	if err := db.db.QueryRow(q, args...).Scan(&n); err != nil {
		t.Fatal(err)
	}
	return n
}

// Every table keyed by repository name is empty for a purged name, and
// a neighbour's rows are untouched.
func TestPurgeRepoEmptiesEveryRepoKeyedTable(t *testing.T) {
	db := testDB(t)
	tables := repoKeyedTables(t, db)
	if len(tables) < 10 {
		t.Fatalf("found %d repo-keyed tables; the schema scan is broken", len(tables))
	}
	for i, table := range tables {
		seedRepoRow(t, db, table, "gone", 2*i+1)
		seedRepoRow(t, db, table, "kept", 2*i+2)
	}
	if err := db.PurgeRepo("gone"); err != nil {
		t.Fatal(err)
	}
	for _, table := range tables {
		left := countWhere(t, db, fmt.Sprintf(`SELECT count(*) FROM %s WHERE repo = ?`, table), "gone")
		if _, keep := purgeKeeps[table]; keep {
			if left != 1 {
				t.Errorf("%s: %d rows for the purged name; it is kept on purpose", table, left)
			}
		} else if left != 0 {
			t.Errorf("%s: %d rows survived the purge", table, left)
		}
		if n := countWhere(t, db, fmt.Sprintf(`SELECT count(*) FROM %s WHERE repo = ?`, table), "kept"); n != 1 {
			t.Errorf("%s: the neighbour's row went too (%d left)", table, n)
		}
	}
}

// Records that name the repository somewhere other than a repo column.
func TestPurgeRepoTakesRecordsKeyedOtherwise(t *testing.T) {
	db := testDB(t)
	for _, name := range []string{"gone", "kept"} {
		if err := db.SetAvatar(Avatar{Kind: AvatarRepo, Name: name, Key: "k-" + name,
			Mime: "image/png", ETag: "e-" + name, Bytes: 1}); err != nil {
			t.Fatal(err)
		}
		if err := db.IdemRemember("ticket.create|"+name+"|dan", "k1", "d", "r"); err != nil {
			t.Fatal(err)
		}
	}
	// Muting is an account preference naming repositories, in the JSON
	// list the inbox writes or the space-separated form it still reads.
	if err := db.SetAccountPref("ann", "mute", `["gone","kept"]`); err != nil {
		t.Fatal(err)
	}
	if err := db.SetAccountPref("bob", "mute", "kept gone"); err != nil {
		t.Fatal(err)
	}
	if err := db.SetAccountPref("cy", "mute", `["gone.x"]`); err != nil {
		t.Fatal(err)
	}
	// "gone" is somebody's companion forum store; its binding must not
	// outlive it and hand the next "gone" to that parent.
	if err := db.BindCompanion("parent", "forum", "gone"); err != nil {
		t.Fatal(err)
	}
	if err := db.BindCompanion("kept", "forum", "kept-forum"); err != nil {
		t.Fatal(err)
	}
	if err := db.PurgeRepo("gone"); err != nil {
		t.Fatal(err)
	}
	if _, ok, _ := db.AvatarOf(AvatarRepo, "gone"); ok {
		t.Error("the repository's avatar survived the purge")
	}
	if _, ok, _ := db.AvatarOf(AvatarRepo, "kept"); !ok {
		t.Error("the neighbour's avatar went too")
	}
	if _, _, ok, _ := db.IdemRecall("ticket.create|gone|dan", "k1"); ok {
		t.Error("a remembered answer for the purged name would replay into its successor")
	}
	if _, _, ok, _ := db.IdemRecall("ticket.create|kept|dan", "k1"); !ok {
		t.Error("the neighbour's remembered answer went too")
	}
	for who, want := range map[string]string{"ann": `["kept"]`, "bob": `["kept"]`, "cy": `["gone.x"]`} {
		if got := db.AccountPref(who, "mute"); got != want {
			t.Errorf("%s's mute list = %s, want %s", who, got, want)
		}
	}
	if _, _, ok, _ := db.CompanionParent("gone"); ok {
		t.Error("the purged name is still bound as a companion store")
	}
	if _, ok, _ := db.Companion("kept", "forum"); !ok {
		t.Error("the neighbour's companion binding went too")
	}
}

// A reporter token minted for a deleted repository authenticates
// nothing for a new repository of the same name.
func TestReporterTokenDiesWithItsRepository(t *testing.T) {
	db := testDB(t)
	if err := db.AddRepo("gone", t.TempDir(), "", Public); err != nil {
		t.Fatal(err)
	}
	tok, err := db.MintReporterToken("gone", "ci")
	if err != nil {
		t.Fatal(err)
	}
	if err := db.PurgeRepo("gone"); err != nil {
		t.Fatal(err)
	}
	if err := db.AddRepo("gone", t.TempDir(), "", Public); err != nil {
		t.Fatal(err)
	}
	if db.CheckReporterToken("gone", tok) {
		t.Fatal("the deleted repository's reporter token authenticates for its successor")
	}
	if toks, _ := db.ReporterTokens("gone"); len(toks) != 0 {
		t.Fatalf("the successor lists %d inherited reporter tokens", len(toks))
	}
}

// A chat channel's robot-line log goes when the channel is purged, and
// only that channel's.
func TestPurgeRobotLines(t *testing.T) {
	db := testDB(t)
	for _, code := range []string{"c0de", "beef"} {
		if err := db.RecordRobotLine(code, 7, now()); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.PurgeRobotLines("c0de"); err != nil {
		t.Fatal(err)
	}
	if n, _ := db.RobotLines("c0de", 0); n != 0 {
		t.Errorf("%d robot lines survived the channel", n)
	}
	if n, _ := db.RobotLines("beef", 0); n != 1 {
		t.Errorf("the neighbour channel has %d robot lines, want 1", n)
	}
}