package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"testing/fstest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sourcecraft.dev/bigbes/sr-ht-core/config"
"sourcecraft.dev/bigbes/sr-ht-ecore/ecoretest"
"sourcecraft.dev/bigbes/sr-ht-ecore/internalauth"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)
// TestMain installs the two pieces of core-go process state the provisioning
// call reads and neither creates: the fixed test keyset, so the token this hook
// seals is one internalauth can open, and a loaded config, whose only job here
// is to fill the internal network list — without it that list is empty, every
// address is external, and the httptest server's loopback caller is refused
// before its token is ever looked at.
func TestMain(m *testing.M) {
config.FS = fstest.MapFS{
"config.ini": &fstest.MapFile{Data: []byte("[sr.ht]\nsite-name=srht.example\n")},
}
config.LoadConfig()
ecoretest.InitCrypto()
os.Exit(m.Run())
}
func samplePush() pushContext {
var pc pushContext
pc.Repo.Name = "widgets"
pc.Repo.OwnerName = "alice"
pc.Repo.Visibility = "private"
return pc
}
// checkInternalAuth runs the real receiving end over the header the hook minted
// — the same internalauth.Identify, against the same pinned ids, that web's
// guard on /internal/repos runs — and returns the caller it identified.
//
// It is the guard itself rather than a decode written here on purpose: this is
// the one test in the repo where both ends of the protocol are present, so it
// is where a drift between them has to fail.
func checkInternalAuth(t *testing.T, r *http.Request) internalauth.Auth {
t.Helper()
auth, err := internalauth.Identify(r, core.InternalClientID, core.InternalNodeID)
require.NoError(t, err, "the header the hook minted must satisfy the guard")
return auth
}
func TestCreateCompanionSignsAndAnnouncesOnCreate(t *testing.T) {
var gotBody map[string]string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/internal/repos" || r.Method != http.MethodPost {
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
}
auth := checkInternalAuth(t, r)
// The call is made on the pushing owner's behalf, so the receiving
// service can see whose push provoked it.
assert.Equal(t, "alice", auth.Name)
raw, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(raw, &gotBody)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"url":"https://dolt.example/~alice/widgets","created":true}`))
}))
defer srv.Close()
var out bytes.Buffer
createCompanion(&out, srv.URL, samplePush())
if gotBody["owner"] != "alice" || gotBody["name"] != "widgets" {
t.Fatalf("unexpected request body: %+v", gotBody)
}
if gotBody["visibility"] != "PRIVATE" {
t.Fatalf("visibility not normalized to PRIVATE: %q", gotBody["visibility"])
}
if !strings.Contains(out.String(), "NOTICE") ||
!strings.Contains(out.String(), "dolt clone https://dolt.example/~alice/widgets") {
t.Fatalf("expected clone notice, got: %q", out.String())
}
}
func TestCreateCompanionQuietWhenExists(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
checkInternalAuth(t, r) // still must be authenticated
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"url":"https://dolt.example/~alice/widgets","created":false}`))
}))
defer srv.Close()
var out bytes.Buffer
createCompanion(&out, srv.URL, samplePush())
if out.Len() != 0 {
t.Fatalf("expected no output for existing companion, got: %q", out.String())
}
}
func TestCreateCompanionWarnsOnError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("boom"))
}))
defer srv.Close()
var out bytes.Buffer
createCompanion(&out, srv.URL, samplePush())
if !strings.Contains(out.String(), "provisioning failed (500)") {
t.Fatalf("expected failure warning, got: %q", out.String())
}
}
func TestNormalizeVisibility(t *testing.T) {
cases := map[string]string{
"private": "PRIVATE", "PUBLIC": "PUBLIC", "Unlisted": "UNLISTED",
"": "", "bogus": "",
}
for in, want := range cases {
if got := normalizeVisibility(in); got != want {
t.Errorf("normalizeVisibility(%q) = %q, want %q", in, got, want)
}
}
}