Skip to content
quarry Artifact [067965cd56]
Login

Artifact [067965cd56]

Artifact 067965cd562f1aec887f262900a691f6de4ac6aa690ffd3f9785c01e8855d3b6:


package chat

import (
	"testing"
	"time"

	natsserver "github.com/nats-io/nats-server/v2/server"

	"craftdesign.group/quarry/internal/events"
)

func room(t *testing.T, ev *events.Server) *Room {
	t.Helper()
	r, err := New(ev.Conn(), ev.JS())
	if err != nil {
		t.Fatal(err)
	}
	return r
}

// Post then History replays the recent window, oldest first; a second
// channel is isolated from the first.
func TestPostAndHistory(t *testing.T) {
	ev, err := events.Start(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer ev.Close()
	r := room(t, ev)

	for _, txt := range []string{"first", "second", "third"} {
		if err := r.Post("repoA", Message{User: "dan", Text: txt}); err != nil {
			t.Fatal(err)
		}
	}
	if err := r.Post("repoB", Message{User: "eve", Text: "other channel"}); err != nil {
		t.Fatal(err)
	}

	hist, err := r.History("repoA", 50)
	if err != nil {
		t.Fatal(err)
	}
	if len(hist) != 3 || hist[0].Text != "first" || hist[2].Text != "third" {
		t.Fatalf("history = %+v", hist)
	}
	if hist[0].At == "" || hist[0].User != "dan" {
		t.Fatalf("message not stamped: %+v", hist[0])
	}
	other, _ := r.History("repoB", 50)
	if len(other) != 1 || other[0].Text != "other channel" {
		t.Fatalf("channel isolation broken: %+v", other)
	}

	// Empty author defaults to anonymous.
	r.Post("repoA", Message{Text: "no name"})
	hist, _ = r.History("repoA", 50)
	if hist[len(hist)-1].User != "anonymous" {
		t.Fatalf("anon default missing: %+v", hist[len(hist)-1])
	}
}

// A live subscriber receives posts as they arrive.
func TestLiveSubscribe(t *testing.T) {
	ev, err := events.Start(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer ev.Close()
	r := room(t, ev)

	ch, cancel := r.Subscribe("repo")
	defer cancel()
	if err := r.Post("repo", Message{User: "dan", Text: "live message"}); err != nil {
		t.Fatal(err)
	}
	select {
	case m := <-ch:
		if m.Text != "live message" || m.User != "dan" {
			t.Fatalf("got %+v", m)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("live message not delivered")
	}
}

// Chat fans out across nodes on a shared external NATS — a post on one
// node reaches a subscriber on another (the multi-node property).
func TestCrossNodeChat(t *testing.T) {
	url := externalURL(t)
	evA, err := events.Connect(url)
	if err != nil {
		t.Fatal(err)
	}
	defer evA.Close()
	evB, err := events.Connect(url)
	if err != nil {
		t.Fatal(err)
	}
	defer evB.Close()
	a, b := room(t, evA), room(t, evB)

	ch, cancel := b.Subscribe("shared")
	defer cancel()
	if err := a.Post("shared", Message{User: "dan", Text: "cross-node hello"}); err != nil {
		t.Fatal(err)
	}
	select {
	case m := <-ch:
		if m.Text != "cross-node hello" {
			t.Fatalf("got %+v", m)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("cross-node chat not delivered")
	}
}

func externalURL(t *testing.T) string {
	t.Helper()
	srv, err := natsserver.NewServer(&natsserver.Options{
		Host: "127.0.0.1", Port: -1, JetStream: true, StoreDir: t.TempDir(),
	})
	if err != nil {
		t.Fatal(err)
	}
	go srv.Start()
	if !srv.ReadyForConnections(10 * time.Second) {
		t.Fatal("external NATS not ready")
	}
	t.Cleanup(srv.Shutdown)
	return srv.ClientURL()
}