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) } } }