Skip to content
quarry Artifact [13d73c1a02]
Login

Artifact [13d73c1a02]

Artifact 13d73c1a020aa8da2448bb745b56b0ebd134691a82be517912cbd1a4ec7040e7:


package oltp

import (
	"errors"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

func bridgeDB(t *testing.T) *DB {
	t.Helper()
	db, err := Open(filepath.Join(t.TempDir(), "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	return db
}

// The credential goes in and is readable only by the poller's own
// accessor; nothing a listing returns carries it.
func TestChatBridgeKeepsTheSecretOffEveryListing(t *testing.T) {
	db := bridgeDB(t)
	if err := db.SetChatBridge("alpha", "https://example.org/repo", "bot", "s3cret"); err != nil {
		t.Fatal(err)
	}
	got, ok, err := db.ChatBridgeFor("alpha")
	if err != nil || !ok {
		t.Fatalf("bridge not stored: ok=%v err=%v", ok, err)
	}
	if got.URL != "https://example.org/repo" || got.User != "bot" {
		t.Fatalf("bridge = %+v", got)
	}
	secret, ok := db.ChatBridgeSecret("alpha")
	if !ok || secret != "s3cret" {
		t.Fatalf("the poller cannot read the credential: %q %v", secret, ok)
	}
	list, err := db.ChatBridges()
	if err != nil || len(list) != 1 || list[0].Repo != "alpha" {
		t.Fatalf("bridges = %+v (%v)", list, err)
	}
}

// Only http(s), the rule every URL quarry fetches on its own
// initiative lives under.
func TestChatBridgeRefusesAForeignScheme(t *testing.T) {
	db := bridgeDB(t)
	for _, bad := range []string{"file:///etc/passwd", "ssh://host/repo", "", "notaurl"} {
		if err := db.SetChatBridge("alpha", bad, "bot", "pw"); err == nil {
			t.Fatalf("%q was accepted as a bridge url", bad)
		}
	}
	if err := db.SetChatBridge("alpha", "https://x.example/r", "", "pw"); err == nil {
		t.Fatal("a bridge with no login was accepted")
	}
}

// Reconfiguring keeps the cursor when the upstream is the same and
// resets it when it is not: msgids belong to the server that minted
// them, and replaying another's numbering imports the wrong messages.
func TestChatBridgeCursorFollowsTheUpstream(t *testing.T) {
	db := bridgeDB(t)
	if err := db.SetChatBridge("alpha", "https://a.example/r", "bot", "pw"); err != nil {
		t.Fatal(err)
	}
	if err := db.SetChatBridgeCursor("alpha", 42, time.Now()); err != nil {
		t.Fatal(err)
	}
	// Same URL, new password: the cursor stands.
	if err := db.SetChatBridge("alpha", "https://a.example/r", "bot", "pw2"); err != nil {
		t.Fatal(err)
	}
	if b, _, _ := db.ChatBridgeFor("alpha"); b.LastMsgID != 42 {
		t.Fatalf("fixing a password reset the cursor to %d", b.LastMsgID)
	}
	// New URL: a different fossil, a different numbering.
	if err := db.SetChatBridge("alpha", "https://b.example/r", "bot", "pw2"); err != nil {
		t.Fatal(err)
	}
	if b, _, _ := db.ChatBridgeFor("alpha"); b.LastMsgID != 0 {
		t.Fatalf("a new upstream kept the old cursor: %d", b.LastMsgID)
	}
}

// A failure is remembered without moving the cursor — a failed poll
// read nothing — and the next message clears it.
func TestChatBridgeErrorDoesNotAdvanceTheCursor(t *testing.T) {
	db := bridgeDB(t)
	if err := db.SetChatBridge("alpha", "https://a.example/r", "bot", "pw"); err != nil {
		t.Fatal(err)
	}
	if err := db.SetChatBridgeCursor("alpha", 7, time.Now()); err != nil {
		t.Fatal(err)
	}
	if err := db.RecordChatBridgeError("alpha", errors.New("upstream is down")); err != nil {
		t.Fatal(err)
	}
	b, _, _ := db.ChatBridgeFor("alpha")
	if b.LastMsgID != 7 || b.LastErr != "upstream is down" {
		t.Fatalf("after a failure: %+v", b)
	}
	if err := db.SetChatBridgeCursor("alpha", 8, time.Now()); err != nil {
		t.Fatal(err)
	}
	if b, _, _ := db.ChatBridgeFor("alpha"); b.LastErr != "" || b.LastMsgID != 8 {
		t.Fatalf("a message did not clear the error: %+v", b)
	}
}

// The msgid map answers what a later mdel needs, prunes with the
// window, and dies with the bridge — a re-attach must not tombstone a
// quarry message on a number the new upstream reused.
func TestChatBridgeMsgMapLivesAndDiesWithTheBridge(t *testing.T) {
	db := bridgeDB(t)
	if err := db.SetChatBridge("alpha", "https://a.example/r", "bot", "pw"); err != nil {
		t.Fatal(err)
	}
	for _, m := range []struct {
		msgid int64
		seq   uint64
	}{{1, 10}, {2, 20}, {3, 30}} {
		if err := db.MapChatBridgeMsg("alpha", m.msgid, m.seq); err != nil {
			t.Fatal(err)
		}
	}
	if seq, ok := db.ChatBridgeSeq("alpha", 2); !ok || seq != 20 {
		t.Fatalf("msgid 2 = %d %v, want 20", seq, ok)
	}
	// Idempotent: a replayed poll must not double anything.
	if err := db.MapChatBridgeMsg("alpha", 2, 999); err != nil {
		t.Fatal(err)
	}
	if seq, _ := db.ChatBridgeSeq("alpha", 2); seq != 20 {
		t.Fatalf("a replay rewrote the mapping to %d", seq)
	}
	if err := db.PruneChatBridgeMsgs("alpha", 25); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.ChatBridgeSeq("alpha", 1); ok {
		t.Fatal("a row outside the window survived the prune")
	}
	if _, ok := db.ChatBridgeSeq("alpha", 3); !ok {
		t.Fatal("the prune took a row still inside the window")
	}
	if err := db.DetachChatBridge("alpha"); err != nil {
		t.Fatal(err)
	}
	if _, ok, _ := db.ChatBridgeFor("alpha"); ok {
		t.Fatal("detach left the bridge configured")
	}
	if _, ok := db.ChatBridgeSeq("alpha", 3); ok {
		t.Fatal("detach left the msgid map behind")
	}
}

// Purging a repository takes its bridge and map with it, like every
// other repo-keyed table.
func TestPurgeRepoTakesTheChatBridge(t *testing.T) {
	db := bridgeDB(t)
	if err := db.SetChatBridge("alpha", "https://a.example/r", "bot", "pw"); err != nil {
		t.Fatal(err)
	}
	if err := db.MapChatBridgeMsg("alpha", 1, 10); err != nil {
		t.Fatal(err)
	}
	if err := db.PurgeRepo("alpha"); err != nil {
		t.Fatal(err)
	}
	if _, ok, _ := db.ChatBridgeFor("alpha"); ok {
		t.Fatal("the bridge survived a repository purge")
	}
	if _, ok := db.ChatBridgeSeq("alpha", 1); ok {
		t.Fatal("the msgid map survived a repository purge")
	}
}

// A changed upstream invalidates the msgid map as well as the cursor.
// Msgids belong to the fossil that minted them, so a map carried
// across would let a new server's msgid tombstone a quarry message the
// old server's msgid had claimed — the hazard DetachChatBridge already
// handles, reached by reconfiguring instead of detaching.
func TestChatBridgeURLChangeClearsTheMsgMap(t *testing.T) {
	db := bridgeDB(t)
	if err := db.SetChatBridge("alpha", "https://a.example/r", "bot", "pw"); err != nil {
		t.Fatal(err)
	}
	if err := db.MapChatBridgeMsg("alpha", 1, 10); err != nil {
		t.Fatal(err)
	}
	// Same upstream: the map is still about the right server.
	if err := db.SetChatBridge("alpha", "https://a.example/r", "bot", "pw2"); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.ChatBridgeSeq("alpha", 1); !ok {
		t.Fatal("changing the password dropped the msgid map")
	}
	if err := db.SetChatBridge("alpha", "https://b.example/r", "bot", "pw2"); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.ChatBridgeSeq("alpha", 1); ok {
		t.Fatal("a new upstream kept the old server's msgid map")
	}
}

// Credentials belong in the columns the poller reads, not in a URL
// that every log line and status row would carry.
func TestChatBridgeRefusesCredentialsInTheURL(t *testing.T) {
	db := bridgeDB(t)
	for _, bad := range []string{
		"https://bot:[email protected]/r",
		"https://[email protected]/r",
	} {
		if err := db.SetChatBridge("alpha", bad, "bot", "pw"); err == nil {
			t.Fatalf("%q was accepted", bad)
		}
	}
}

// A stored error is rendered on an admin page, so any URL inside it
// arrives without its credentials — stdlib transport errors quote the
// whole request URL.
func TestRecordChatBridgeErrorRedactsCredentials(t *testing.T) {
	db := bridgeDB(t)
	if err := db.SetChatBridge("alpha", "https://a.example/r", "bot", "pw"); err != nil {
		t.Fatal(err)
	}
	if err := db.RecordChatBridgeError("alpha",
		errors.New(`Get "https://bot:[email protected]/r/chat-poll": dial tcp: refused`)); err != nil {
		t.Fatal(err)
	}
	b, _, _ := db.ChatBridgeFor("alpha")
	if strings.Contains(b.LastErr, "hunter2") || strings.Contains(b.LastErr, "bot:") {
		t.Fatalf("the stored error carries the credential: %q", b.LastErr)
	}
	if !strings.Contains(b.LastErr, "https://a.example/r/chat-poll") {
		t.Fatalf("redaction took the host with it: %q", b.LastErr)
	}
	if !strings.Contains(b.LastErr, "dial tcp: refused") {
		t.Fatalf("redaction took the reason with it: %q", b.LastErr)
	}
}