package api import ( "context" "encoding/json" "errors" "net/http" "net/url" "strings" "sync" "testing" "tangled.org/core/api/org_tangled" ) type stubRunner struct { mu sync.Mutex calls []string out string err error } func (r *stubRunner) Run(_ context.Context, _ string, _ []string, name string, args ...string) (string, error) { r.mu.Lock() defer r.mu.Unlock() r.calls = append(r.calls, name+" "+strings.Join(args, " ")) return r.out, r.err } func (r *stubRunner) LookPath(string) (string, error) { return "/usr/bin/git", nil } func TestRepoNameFromURL(t *testing.T) { tests := []struct { url string want string wantErr string }{ {"https://example.com/alice/repo.git", "repo", ""}, {"https://example.com/alice/repo", "repo", ""}, {"https://example.com/alice/repo/", "repo", ""}, {"https://example.com/repo.git", "repo", ""}, {"https://example.com/alice/repo.git/", "repo", ""}, {"ssh://git@example.com/alice/repo.git", "repo", ""}, {"http://example.com/alice/repo", "repo", ""}, {"https://example.com/", "", "must name a repository path"}, } for _, tt := range tests { got, err := RepoNameFromURL(tt.url) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("RepoNameFromURL(%q) = %q, %v; want error containing %q", tt.url, got, err, tt.wantErr) } continue } if err != nil { t.Fatalf("RepoNameFromURL(%q): %v", tt.url, err) } if got != tt.want { t.Fatalf("RepoNameFromURL(%q) = %q, want %q", tt.url, got, tt.want) } } } func TestDescribeSource(t *testing.T) { const path = "/xrpc/" + org_tangled.TempMigratorDescribeSourceNSID const symref = "ref: refs/heads/main\tHEAD\n1a2b3c4d5e6f\tHEAD\n" t.Run("needs the same auth as every other method", func(t *testing.T) { ts := setupTestServer(t) rec := ts.do(t, "GET", path+"?sourceUrl=https://example.com/a/repo.git", "", nil) if rec.Code != http.StatusForbidden { t.Fatalf("want 403, got %d", rec.Code) } }) t.Run("missing sourceUrl is an InvalidRequest", func(t *testing.T) { ts := setupTestServer(t) rec := ts.do(t, "GET", path, ts.token(t, ts.alice, org_tangled.TempMigratorDescribeSourceNSID), nil) if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "InvalidRequest") { t.Fatalf("want 400 InvalidRequest, got %d (%s)", rec.Code, rec.Body.String()) } }) t.Run("a url the importer would refuse is InvalidSource", func(t *testing.T) { ts := setupTestServer(t) for _, bad := range []string{ "http://example.com/a/repo.git", // not https "https://alice:secret@example.com/a/repo.git", // credentials "https://example.com/a/repo.git?ref=main", // query "https://example.com", // no repository path } { rec := ts.do(t, "GET", path+"?sourceUrl="+url.QueryEscape(bad), ts.token(t, ts.alice, org_tangled.TempMigratorDescribeSourceNSID), nil) if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "InvalidSource") { t.Fatalf("%s: want 400 InvalidSource, got %d (%s)", bad, rec.Code, rec.Body.String()) } } }) t.Run("a repository that cannot be reached is UnreachableSource", func(t *testing.T) { ts := setupTestServer(t) runner := &stubRunner{err: errors.New("exit status 128 (output: fatal: could not read from remote)")} ts.server.gitRunner = runner rec := ts.do(t, "GET", path+"?sourceUrl="+url.QueryEscape("https://example.com/a/repo.git"), ts.token(t, ts.alice, org_tangled.TempMigratorDescribeSourceNSID), nil) if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "UnreachableSource") { t.Fatalf("want 400 UnreachableSource, got %d (%s)", rec.Code, rec.Body.String()) } }) t.Run("success answers with the derived name and the remote default branch", func(t *testing.T) { ts := setupTestServer(t) runner := &stubRunner{out: symref} ts.server.gitRunner = runner source := "https://example.com/alice/repo.git" rec := ts.do(t, "GET", path+"?sourceUrl="+url.QueryEscape(source), ts.token(t, ts.alice, org_tangled.TempMigratorDescribeSourceNSID), nil) if rec.Code != http.StatusOK { t.Fatalf("want 200, got %d (%s)", rec.Code, rec.Body.String()) } var out org_tangled.TempMigratorDescribeSource_Output if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { t.Fatal(err) } if out.Name != "repo" || out.DefaultBranch != "main" { t.Fatalf("output = %+v, want name repo and defaultBranch main", out) } runner.mu.Lock() defer runner.mu.Unlock() if len(runner.calls) != 1 || !strings.Contains(runner.calls[0], "--symref "+source) { t.Fatalf("git calls = %v, want one ls-remote --symref against the source url", runner.calls) } }) }