Skip to content
quarry Artifact [26b82d7a23]
Login

Artifact [26b82d7a23]

Artifact 26b82d7a239ae43de9a9e013a20bbb6e0d6b915835bfd2f78e5972081ed96e87:


package oltp

import (
	"errors"
	"path/filepath"
	"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")
	}
}