package main
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/fernet/fernet-go"
"github.com/vaughan0/go-ini"
"sourcecraft.dev/bigbes/sr-ht-core/crypto"
)
// initTestCrypto installs a random network key + webhooks seed into the shared
// crypto globals, mirroring the sr-ht-core test pattern, so createCompanion's
// crypto.Encrypt and the test server's DecryptWithExpiration share a keyset.
func initTestCrypto(t *testing.T) {
t.Helper()
var fk fernet.Key
if err := fk.Generate(); err != nil {
t.Fatalf("fernet generate: %v", err)
}
seed := make([]byte, ed25519.SeedSize)
for i := range seed {
seed[i] = byte(i + 1)
}
crypto.InitCrypto(ini.File{
"sr.ht": ini.Section{"network-key": fk.Encode()},
"webhooks": ini.Section{"private-key": base64.StdEncoding.EncodeToString(seed)},
})
}
func samplePush() pushContext {
var pc pushContext
pc.Repo.Name = "widgets"
pc.Repo.OwnerName = "alice"
pc.Repo.Visibility = "private"
return pc
}
// decodeInternalAuth verifies an incoming "Internal <token>" header the same way
// dolt.sr.ht's internalAuthGuard does and returns the decoded claims.
func decodeInternalAuth(t *testing.T, r *http.Request) internalAuth {
t.Helper()
parts := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "internal") {
t.Fatalf("missing/invalid Internal auth header: %q", r.Header.Get("Authorization"))
}
payload := crypto.DecryptWithExpiration([]byte(parts[1]), 30*time.Second)
if payload == nil {
t.Fatalf("auth token did not decrypt (wrong key or expired)")
}
var ia internalAuth
if err := json.Unmarshal(payload, &ia); err != nil {
t.Fatalf("auth payload not JSON: %v", err)
}
return ia
}
func TestCreateCompanionSignsAndAnnouncesOnCreate(t *testing.T) {
initTestCrypto(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)
}
ia := decodeInternalAuth(t, r)
if ia.ClientID != "git.sr.ht" || ia.NodeID != "dolt-git-hook" {
t.Errorf("unexpected auth claims: %+v", ia)
}
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) {
initTestCrypto(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
decodeInternalAuth(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) {
initTestCrypto(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)
}
}
}