package api
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
)
// memBridges is a ChatBridgeStore that remembers what it was told,
// credential included, so a test can prove the credential arrives at
// the store and never leaves it.
type memBridges struct {
rows map[string]ChatBridge
secret map[string]string
}
func newMemBridges() *memBridges {
return &memBridges{rows: map[string]ChatBridge{}, secret: map[string]string{}}
}
func (m *memBridges) SetChatBridge(repo, url, user, secret string) error {
if !strings.HasPrefix(url, "http") {
return errors.New("bridge url must be http or https")
}
m.rows[repo] = ChatBridge{Repo: repo, URL: url, User: user}
m.secret[repo] = secret
return nil
}
func (m *memBridges) DetachChatBridge(repo string) error {
delete(m.rows, repo)
delete(m.secret, repo)
return nil
}
func (m *memBridges) ChatBridgeFor(repo string) (ChatBridge, bool, error) {
b, ok := m.rows[repo]
return b, ok, nil
}
func (m *memBridges) ChatBridges() ([]ChatBridge, error) {
out := make([]ChatBridge, 0, len(m.rows))
for _, b := range m.rows {
out = append(out, b)
}
return out, nil
}
func bridgeRegistry(t *testing.T) (*ops.Registry, *memBridges) {
t.Helper()
repos := testRepos(wikiIndex(t))
mustRepo(t, repos, "test").Code = "codeXYZ"
store := newMemBridges()
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: repos, Chat: &fakeChat{},
ChatBridges: store}); err != nil {
t.Fatal(err)
}
return r, store
}
// The password reaches the store and appears in nothing that comes
// back — not in the Out struct, and not in its JSON encoding, which is
// what a page would actually render.
func TestChatBridgeNeverReturnsTheCredential(t *testing.T) {
r, store := bridgeRegistry(t)
out, err := r.Invoke(context.Background(), "mirror.chatbridge", chatBridgeIn{
Repo: "test", URL: "https://fossil.example/repo", User: "bot", Password: "s3cretpw"})
if err != nil {
t.Fatal(err)
}
if store.secret["test"] != "s3cretpw" {
t.Fatalf("the credential did not reach the store: %q", store.secret["test"])
}
wire, err := json.Marshal(out)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(wire), "s3cretpw") {
t.Fatalf("the credential came back on the wire: %s", wire)
}
got := out.(chatBridgeOut)
if got.Bridge.URL != "https://fossil.example/repo" || got.Bridge.User != "bot" {
t.Fatalf("bridge out = %+v", got.Bridge)
}
}
// A bridge needs all three of url, user and password; a partial one is
// ErrInvalid, not a row that can never connect.
func TestChatBridgeRefusesAPartialConfiguration(t *testing.T) {
r, _ := bridgeRegistry(t)
for _, in := range []chatBridgeIn{
{Repo: "test", URL: "https://x.example/r", User: "bot"},
{Repo: "test", URL: "https://x.example/r", Password: "pw"},
{Repo: "test", User: "bot", Password: "pw"},
{Repo: "test", URL: "ssh://x.example/r", User: "bot", Password: "pw"},
} {
if _, err := r.Invoke(context.Background(), "mirror.chatbridge", in); !errors.Is(err, ops.ErrInvalid) {
t.Fatalf("%+v: want ErrInvalid, got %v", in, err)
}
}
}
// Detaching forgets the bridge, and the op says so.
func TestChatBridgeDetach(t *testing.T) {
r, store := bridgeRegistry(t)
if _, err := r.Invoke(context.Background(), "mirror.chatbridge", chatBridgeIn{
Repo: "test", URL: "https://x.example/r", User: "bot", Password: "pw"}); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(context.Background(), "mirror.chatbridge",
chatBridgeIn{Repo: "test", Detach: true})
if err != nil {
t.Fatal(err)
}
if !out.(chatBridgeOut).Detached {
t.Fatalf("detach out = %+v", out)
}
if _, ok := store.rows["test"]; ok {
t.Fatal("detach left the bridge configured")
}
if _, ok := store.secret["test"]; ok {
t.Fatal("detach left the credential behind")
}
}
// The op is admin-only and named where the RFC says.
func TestChatBridgeSurface(t *testing.T) {
r, _ := bridgeRegistry(t)
op, ok := r.Lookup("mirror.chatbridge")
if !ok {
t.Fatal("mirror.chatbridge is not registered")
}
if op.Caps != "a" || op.Auth != ops.Admin || op.Method != "POST" ||
op.Pattern != "/api/v1/r/{repo}/mirror/chatbridge" {
t.Fatalf("surface = caps %q auth %v %s %s", op.Caps, op.Auth, op.Method, op.Pattern)
}
}
// A bridge's state is the one word a status line needs, and it tells
// "quiet" from "broken".
func TestChatBridgeState(t *testing.T) {
for _, c := range []struct {
b ChatBridge
want string
}{
{ChatBridge{}, "connecting"},
{ChatBridge{At: "2026-08-29T00:00:00Z"}, "connected"},
{ChatBridge{At: "2026-08-29T00:00:00Z", LastErr: "boom"}, "failing"},
} {
if got := c.b.State(); got != c.want {
t.Fatalf("%+v state = %q, want %q", c.b, got, c.want)
}
}
}