webctx 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +97 -0
- package/CONTRIBUTORS.md +126 -0
- package/LICENSE +202 -0
- package/Makefile +69 -0
- package/README.md +121 -0
- package/bin/webctx.js +28 -0
- package/cmd/webctx/main.go +11 -0
- package/docs/porting-status.md +173 -0
- package/go.mod +3 -0
- package/internal/app/app.go +139 -0
- package/internal/app/app_test.go +194 -0
- package/internal/app/github.go +2084 -0
- package/internal/app/github_acceptance_test.go +166 -0
- package/internal/app/github_actions.go +1008 -0
- package/internal/app/github_actions_test.go +484 -0
- package/internal/app/github_activity_deployments.go +503 -0
- package/internal/app/github_activity_deployments_test.go +321 -0
- package/internal/app/github_commits.go +1004 -0
- package/internal/app/github_commits_test.go +503 -0
- package/internal/app/github_discussions_gists.go +1156 -0
- package/internal/app/github_discussions_gists_test.go +425 -0
- package/internal/app/github_issues.go +1616 -0
- package/internal/app/github_issues_test.go +731 -0
- package/internal/app/github_packages_projects.go +429 -0
- package/internal/app/github_packages_projects_test.go +168 -0
- package/internal/app/github_phase9_overview_stress_test.go +178 -0
- package/internal/app/github_pull_views.go +1141 -0
- package/internal/app/github_pull_views_test.go +603 -0
- package/internal/app/github_pulls.go +1459 -0
- package/internal/app/github_pulls_test.go +715 -0
- package/internal/app/github_refs.go +621 -0
- package/internal/app/github_refs_test.go +260 -0
- package/internal/app/github_search_profiles.go +505 -0
- package/internal/app/github_search_profiles_test.go +352 -0
- package/internal/app/github_test.go +888 -0
- package/internal/app/scrape.go +264 -0
- package/internal/app/tools.go +670 -0
- package/internal/buildinfo/buildinfo.go +16 -0
- package/package.json +66 -0
- package/scripts/postinstall.js +137 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/http/httptest"
|
|
9
|
+
"net/url"
|
|
10
|
+
"strings"
|
|
11
|
+
"sync/atomic"
|
|
12
|
+
"testing"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func TestParseGitHubSearchAndProfileTargets(t *testing.T) {
|
|
16
|
+
for _, tt := range []struct {
|
|
17
|
+
raw string
|
|
18
|
+
kind GitHubTargetKind
|
|
19
|
+
owner string
|
|
20
|
+
repo string
|
|
21
|
+
name string
|
|
22
|
+
}{
|
|
23
|
+
{raw: "https://github.com/search?q=webctx&type=repositories", kind: GitHubTargetSearch},
|
|
24
|
+
{raw: "https://github.com/o/r/search?q=reader&type=code", kind: GitHubTargetSearch, owner: "o", repo: "r"},
|
|
25
|
+
{raw: "https://github.com/octocat", kind: GitHubTargetProfile, owner: "octocat"},
|
|
26
|
+
{raw: "https://github.com/openai?tab=repositories", kind: GitHubTargetProfile, owner: "openai"},
|
|
27
|
+
{raw: "https://github.com/orgs/openai/people", kind: GitHubTargetProfile, owner: "openai", name: "people"},
|
|
28
|
+
} {
|
|
29
|
+
target := parseGitHubTarget(tt.raw)
|
|
30
|
+
if target == nil || target.Kind != tt.kind || target.Owner != tt.owner || target.Repo != tt.repo || target.Name != tt.name {
|
|
31
|
+
t.Fatalf("target %s => %#v", tt.raw, target)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for _, raw := range []string{
|
|
35
|
+
"https://github.com/search",
|
|
36
|
+
"https://github.com/search?q=x&type=unknown",
|
|
37
|
+
"https://github.com/search?q=x&type=repositories&unknown=y",
|
|
38
|
+
"https://github.com/settings",
|
|
39
|
+
"https://github.com/login",
|
|
40
|
+
"https://github.com/marketplace",
|
|
41
|
+
} {
|
|
42
|
+
if target := parseGitHubTarget(raw); target != nil {
|
|
43
|
+
t.Fatalf("unsupported/global route was claimed: %s => %#v", raw, target)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func TestGitHubSearchRepositoryScopeSortOrderAndNavigation(t *testing.T) {
|
|
49
|
+
var server *httptest.Server
|
|
50
|
+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
51
|
+
if r.URL.Path != "/search/repositories" {
|
|
52
|
+
t.Fatalf("unexpected search path %s", r.URL.Path)
|
|
53
|
+
}
|
|
54
|
+
for key, want := range map[string]string{"q": "reader repo:o/r", "sort": "stars", "order": "desc", "page": "2", "per_page": "8"} {
|
|
55
|
+
if got := r.URL.Query().Get(key); got != want {
|
|
56
|
+
t.Errorf("search query %s=%q want %q (%s)", key, got, want, r.URL.RawQuery)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
w.Header().Set("Content-Type", "application/json")
|
|
60
|
+
w.Header().Set("Link", fmt.Sprintf(`<%s/search/repositories?q=reader+repo%%3Ao%%2Fr&sort=stars&order=desc&per_page=8&page=1>; rel="prev", <%s/search/repositories?q=reader+repo%%3Ao%%2Fr&sort=stars&order=desc&per_page=8&page=3>; rel="next"`, server.URL, server.URL))
|
|
61
|
+
_, _ = io.WriteString(w, `{"total_count":1200,"incomplete_results":true,"items":[{"full_name":"o/r","html_url":"https://github.com/o/r","description":"repo","language":"Go","stargazers_count":12,"forks_count":3,"archived":false}]}`)
|
|
62
|
+
}))
|
|
63
|
+
defer server.Close()
|
|
64
|
+
target := parseGitHubTarget("https://github.com/o/r/search?q=reader&type=repositories&s=stars&o=desc&p=2")
|
|
65
|
+
out, err := readGitHubSearch(context.Background(), testGitHubClient(server.URL, server.URL, ""), target)
|
|
66
|
+
if err != nil {
|
|
67
|
+
t.Fatal(err)
|
|
68
|
+
}
|
|
69
|
+
for _, want := range []string{
|
|
70
|
+
`repository_scope: "o/r"`, `total_count: 1200`, `incomplete_results: true`, `provider_result_ceiling: 1000`,
|
|
71
|
+
"[o/r](https://github.com/o/r)", "12 stars", "GitHub marked these search results incomplete", "at most 1000 results",
|
|
72
|
+
"Previous: https://github.com/o/r/search?", "p=1", "Next: https://github.com/o/r/search?", "p=3",
|
|
73
|
+
} {
|
|
74
|
+
if !strings.Contains(out, want) {
|
|
75
|
+
t.Fatalf("search output missing %q:\n%s", want, out)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func TestGitHubSearchIssueAndPRQualifiers(t *testing.T) {
|
|
81
|
+
for _, tt := range []struct {
|
|
82
|
+
typeValue string
|
|
83
|
+
want string
|
|
84
|
+
notWant string
|
|
85
|
+
}{
|
|
86
|
+
{typeValue: "issues", want: "is:issue", notWant: "is:pr"},
|
|
87
|
+
{typeValue: "pullrequests", want: "is:pr", notWant: "is:issue"},
|
|
88
|
+
} {
|
|
89
|
+
t.Run(tt.typeValue, func(t *testing.T) {
|
|
90
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
91
|
+
if r.URL.Path != "/search/issues" {
|
|
92
|
+
t.Fatalf("issue/PR search endpoint=%s", r.URL.Path)
|
|
93
|
+
}
|
|
94
|
+
q := r.URL.Query().Get("q")
|
|
95
|
+
if !strings.Contains(q, tt.want) || strings.Contains(q, tt.notWant) {
|
|
96
|
+
t.Fatalf("provider query=%q want %q without %q", q, tt.want, tt.notWant)
|
|
97
|
+
}
|
|
98
|
+
w.Header().Set("Content-Type", "application/json")
|
|
99
|
+
_, _ = io.WriteString(w, `{"total_count":1,"incomplete_results":false,"items":[{"number":7,"title":"Result","state":"open","html_url":"https://github.com/o/r/issues/7","user":{"login":"alice"},"updated_at":"2026-08-01T00:00:00Z"}]}`)
|
|
100
|
+
}))
|
|
101
|
+
defer server.Close()
|
|
102
|
+
out, err := readGitHubSearch(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/search?q=bug&type="+tt.typeValue))
|
|
103
|
+
if err != nil {
|
|
104
|
+
t.Fatal(err)
|
|
105
|
+
}
|
|
106
|
+
if !strings.Contains(out, "[#7 Result]") || !strings.Contains(out, "@alice") {
|
|
107
|
+
t.Fatalf("search result missing:\n%s", out)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func TestGitHubSearchCompactResultTypes(t *testing.T) {
|
|
114
|
+
tests := []struct {
|
|
115
|
+
typeValue string
|
|
116
|
+
item string
|
|
117
|
+
want []string
|
|
118
|
+
}{
|
|
119
|
+
{typeValue: "code", item: `{"name":"x.go","path":"internal/x.go","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","html_url":"https://github.com/o/r/blob/a/internal/x.go","repository":{"full_name":"o/r"}}`, want: []string{"internal/x.go", "o/r", "`aaaaaaaaaaaa`"}},
|
|
120
|
+
{typeValue: "commits", item: `{"sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","html_url":"https://github.com/o/r/commit/b","commit":{"message":"Subject\nBody","author":{"date":"2026-08-01T00:00:00Z"}},"repository":{"full_name":"o/r"}}`, want: []string{"`bbbbbbbbbbbb`", "Subject", "o/r"}},
|
|
121
|
+
{typeValue: "users", item: `{"login":"alice","type":"User","html_url":"https://github.com/alice"}`, want: []string{"@alice", "User"}},
|
|
122
|
+
}
|
|
123
|
+
for _, tt := range tests {
|
|
124
|
+
t.Run(tt.typeValue, func(t *testing.T) {
|
|
125
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
126
|
+
w.Header().Set("Content-Type", "application/json")
|
|
127
|
+
_, _ = io.WriteString(w, `{"total_count":1,"incomplete_results":false,"items":[`+tt.item+`]}`)
|
|
128
|
+
}))
|
|
129
|
+
defer server.Close()
|
|
130
|
+
out, err := readGitHubSearch(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/search?q=x&type="+tt.typeValue))
|
|
131
|
+
if err != nil {
|
|
132
|
+
t.Fatal(err)
|
|
133
|
+
}
|
|
134
|
+
for _, want := range tt.want {
|
|
135
|
+
if !strings.Contains(out, want) {
|
|
136
|
+
t.Fatalf("%s search missing %q:\n%s", tt.typeValue, want, out)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func TestGitHubSearchRateLimitUsesSearchResource(t *testing.T) {
|
|
144
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
145
|
+
w.Header().Set("X-RateLimit-Remaining", "0")
|
|
146
|
+
w.Header().Set("X-RateLimit-Resource", "search")
|
|
147
|
+
w.Header().Set("X-RateLimit-Reset", "1893456000")
|
|
148
|
+
w.WriteHeader(http.StatusForbidden)
|
|
149
|
+
_, _ = io.WriteString(w, `{"message":"provider body must not leak"}`)
|
|
150
|
+
}))
|
|
151
|
+
defer server.Close()
|
|
152
|
+
result := readGitHubNativeWithClient(context.Background(), parseGitHubTarget("https://github.com/search?q=x&type=repositories"), testGitHubClient(server.URL, server.URL, ""))
|
|
153
|
+
if result.Outcome != GitHubNativeFailure || result.Err == nil || !strings.Contains(strings.ToLower(result.Err.Error()), "search") || strings.Contains(result.Err.Error(), "provider body") {
|
|
154
|
+
t.Fatalf("Search rate-limit resource/error wrong: %#v", result)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func TestUserProfileResolvesProviderTypeWithoutOrgProbe(t *testing.T) {
|
|
159
|
+
var calls int32
|
|
160
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
161
|
+
atomic.AddInt32(&calls, 1)
|
|
162
|
+
if r.URL.Path != "/users/alice" {
|
|
163
|
+
t.Fatalf("User profile made guessed/org request: %s", r.URL.Path)
|
|
164
|
+
}
|
|
165
|
+
w.Header().Set("Content-Type", "application/json")
|
|
166
|
+
_, _ = io.WriteString(w, `{"login":"alice","type":"User","name":"Alice","bio":"Builder","public_repos":12,"public_gists":3,"followers":4,"following":5,"html_url":"https://github.com/alice","created_at":"2020-01-01T00:00:00Z"}`)
|
|
167
|
+
}))
|
|
168
|
+
defer server.Close()
|
|
169
|
+
out, err := readGitHubProfile(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/alice"))
|
|
170
|
+
if err != nil {
|
|
171
|
+
t.Fatal(err)
|
|
172
|
+
}
|
|
173
|
+
for _, want := range []string{`login: "alice"`, `type: "User"`, "public_repositories: 12", "public_gists: 3", "followers: 4", "# alice", "Builder", "?tab=repositories", "?tab=stars"} {
|
|
174
|
+
if !strings.Contains(out, want) {
|
|
175
|
+
t.Fatalf("User profile missing %q:\n%s", want, out)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if atomic.LoadInt32(&calls) != 1 {
|
|
179
|
+
t.Fatalf("User profile should require one type-resolution call, got %d", calls)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func TestOrganizationProfileUsesProviderTypeThenOrgEndpoint(t *testing.T) {
|
|
184
|
+
var sequence []string
|
|
185
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
186
|
+
sequence = append(sequence, r.URL.Path)
|
|
187
|
+
w.Header().Set("Content-Type", "application/json")
|
|
188
|
+
switch r.URL.Path {
|
|
189
|
+
case "/users/acme":
|
|
190
|
+
_, _ = io.WriteString(w, `{"login":"acme","type":"Organization","html_url":"https://github.com/acme"}`)
|
|
191
|
+
case "/orgs/acme":
|
|
192
|
+
_, _ = io.WriteString(w, `{"login":"acme","name":"Acme","description":"ignored","blog":"https://acme.test","location":"Earth","public_repos":42,"html_url":"https://github.com/acme","created_at":"2019-01-01T00:00:00Z"}`)
|
|
193
|
+
default:
|
|
194
|
+
t.Fatalf("unexpected org profile request %s", r.URL.Path)
|
|
195
|
+
}
|
|
196
|
+
}))
|
|
197
|
+
defer server.Close()
|
|
198
|
+
out, err := readGitHubProfile(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/acme"))
|
|
199
|
+
if err != nil {
|
|
200
|
+
t.Fatal(err)
|
|
201
|
+
}
|
|
202
|
+
if strings.Join(sequence, ",") != "/users/acme,/orgs/acme" || !strings.Contains(out, `type: "Organization"`) || !strings.Contains(out, "public_repositories: 42") || !strings.Contains(out, "/orgs/acme/people") {
|
|
203
|
+
t.Fatalf("provider-resolved Organization profile wrong requests=%v:\n%s", sequence, out)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func TestUserProfileTabsAreBoundedToSelectedEndpoint(t *testing.T) {
|
|
208
|
+
for _, tt := range []struct {
|
|
209
|
+
tab string
|
|
210
|
+
endpoint string
|
|
211
|
+
body string
|
|
212
|
+
want string
|
|
213
|
+
}{
|
|
214
|
+
{tab: "repositories", endpoint: "/users/alice/repos", body: `[{"full_name":"alice/r","html_url":"https://github.com/alice/r","stargazers_count":2}]`, want: "[alice/r]"},
|
|
215
|
+
{tab: "stars", endpoint: "/users/alice/starred", body: `[{"full_name":"o/r","html_url":"https://github.com/o/r","stargazers_count":9}]`, want: "[o/r]"},
|
|
216
|
+
{tab: "gists", endpoint: "/users/alice/gists", body: `[{"id":"abc","html_url":"https://gist.github.com/alice/abc","description":"demo","files":{"a.txt":{"filename":"a.txt"}}}]`, want: "[abc]"},
|
|
217
|
+
{tab: "followers", endpoint: "/users/alice/followers", body: `[{"login":"bob","type":"User","html_url":"https://github.com/bob"}]`, want: "@bob"},
|
|
218
|
+
{tab: "following", endpoint: "/users/alice/following", body: `[{"login":"carol","type":"User","html_url":"https://github.com/carol"}]`, want: "@carol"},
|
|
219
|
+
} {
|
|
220
|
+
t.Run(tt.tab, func(t *testing.T) {
|
|
221
|
+
var listCalls int32
|
|
222
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
223
|
+
w.Header().Set("Content-Type", "application/json")
|
|
224
|
+
if r.URL.Path == "/users/alice" {
|
|
225
|
+
_, _ = io.WriteString(w, `{"login":"alice","type":"User"}`)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
atomic.AddInt32(&listCalls, 1)
|
|
229
|
+
if r.URL.Path != tt.endpoint || r.URL.Query().Get("per_page") != "8" || r.URL.Query().Get("page") != "2" {
|
|
230
|
+
t.Fatalf("tab %s request %s?%s", tt.tab, r.URL.Path, r.URL.RawQuery)
|
|
231
|
+
}
|
|
232
|
+
_, _ = io.WriteString(w, tt.body)
|
|
233
|
+
}))
|
|
234
|
+
defer server.Close()
|
|
235
|
+
out, err := readGitHubProfile(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/alice?tab="+tt.tab+"&page=2"))
|
|
236
|
+
if err != nil {
|
|
237
|
+
t.Fatal(err)
|
|
238
|
+
}
|
|
239
|
+
if atomic.LoadInt32(&listCalls) != 1 || !strings.Contains(out, `view: "`+tt.tab+`"`) || !strings.Contains(out, tt.want) {
|
|
240
|
+
t.Fatalf("tab output wrong:\n%s", out)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func TestOrganizationRepositoriesAndPublicPeople(t *testing.T) {
|
|
247
|
+
for _, tt := range []struct {
|
|
248
|
+
raw string
|
|
249
|
+
endpoint string
|
|
250
|
+
want string
|
|
251
|
+
}{
|
|
252
|
+
{raw: "https://github.com/acme?tab=repositories", endpoint: "/orgs/acme/repos", want: "[acme/r]"},
|
|
253
|
+
{raw: "https://github.com/orgs/acme/people", endpoint: "/orgs/acme/public_members", want: "@member"},
|
|
254
|
+
} {
|
|
255
|
+
t.Run(tt.endpoint, func(t *testing.T) {
|
|
256
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
257
|
+
w.Header().Set("Content-Type", "application/json")
|
|
258
|
+
switch r.URL.Path {
|
|
259
|
+
case "/users/acme":
|
|
260
|
+
_, _ = io.WriteString(w, `{"login":"acme","type":"Organization"}`)
|
|
261
|
+
case "/orgs/acme":
|
|
262
|
+
_, _ = io.WriteString(w, `{"login":"acme"}`)
|
|
263
|
+
case tt.endpoint:
|
|
264
|
+
if strings.HasSuffix(tt.endpoint, "/repos") {
|
|
265
|
+
_, _ = io.WriteString(w, `[{"full_name":"acme/r","html_url":"https://github.com/acme/r","stargazers_count":1}]`)
|
|
266
|
+
} else {
|
|
267
|
+
_, _ = io.WriteString(w, `[{"login":"member","type":"User","html_url":"https://github.com/member"}]`)
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
t.Fatalf("unexpected org tab request %s", r.URL.Path)
|
|
271
|
+
}
|
|
272
|
+
}))
|
|
273
|
+
defer server.Close()
|
|
274
|
+
out, err := readGitHubProfile(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget(tt.raw))
|
|
275
|
+
if err != nil {
|
|
276
|
+
t.Fatal(err)
|
|
277
|
+
}
|
|
278
|
+
if !strings.Contains(out, tt.want) {
|
|
279
|
+
t.Fatalf("org tab output missing %q:\n%s", tt.want, out)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func TestInvalidProfileTabFailsAfterTypeResolutionBeforeListRead(t *testing.T) {
|
|
286
|
+
var listCalls int32
|
|
287
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
288
|
+
w.Header().Set("Content-Type", "application/json")
|
|
289
|
+
if r.URL.Path == "/users/alice" {
|
|
290
|
+
_, _ = io.WriteString(w, `{"login":"alice","type":"User"}`)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
atomic.AddInt32(&listCalls, 1)
|
|
294
|
+
}))
|
|
295
|
+
defer server.Close()
|
|
296
|
+
_, err := readGitHubProfile(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/alice?tab=people"))
|
|
297
|
+
if err == nil || !strings.Contains(err.Error(), "User profile tab") || atomic.LoadInt32(&listCalls) != 0 {
|
|
298
|
+
t.Fatalf("invalid User tab handling err=%v listCalls=%d", err, listCalls)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
func TestProfileProvider404StaysNative(t *testing.T) {
|
|
303
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
304
|
+
w.WriteHeader(http.StatusNotFound)
|
|
305
|
+
_, _ = io.WriteString(w, `{"message":"private provider body"}`)
|
|
306
|
+
}))
|
|
307
|
+
defer server.Close()
|
|
308
|
+
result := readGitHubNativeWithClient(context.Background(), parseGitHubTarget("https://github.com/nobody"), testGitHubClient(server.URL, server.URL, ""))
|
|
309
|
+
if result.Outcome != GitHubNativeFailure || result.Err == nil || strings.Contains(result.Err.Error(), "private provider body") {
|
|
310
|
+
t.Fatalf("profile native failure boundary regressed: %#v", result)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
func TestSearchNavigationKeepsCopiedUIQuery(t *testing.T) {
|
|
315
|
+
target := parseGitHubTarget("https://github.com/search?q=go+cli&type=repositories&s=stars&o=desc&p=2")
|
|
316
|
+
links := GitHubLinkRelations{
|
|
317
|
+
"prev": "https://api.github.com/search/repositories?q=go+cli&page=1",
|
|
318
|
+
"next": "https://api.github.com/search/repositories?q=go+cli&page=3",
|
|
319
|
+
}
|
|
320
|
+
lines := strings.Join(renderGitHubSearchNavigation(target, links), "\n")
|
|
321
|
+
for _, want := range []string{"q=go+cli", "type=repositories", "s=stars", "o=desc", "p=1", "p=3"} {
|
|
322
|
+
if !strings.Contains(lines, want) {
|
|
323
|
+
t.Fatalf("search navigation lost %q:\n%s", want, lines)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func TestSupportedSearchURLQueryValidation(t *testing.T) {
|
|
329
|
+
valid := url.Values{"q": []string{"x"}, "type": []string{"repositories"}, "p": []string{"2"}}
|
|
330
|
+
if !isSupportedGitHubSearchURLQuery(valid) {
|
|
331
|
+
t.Fatal("valid search query rejected")
|
|
332
|
+
}
|
|
333
|
+
invalid := url.Values{"q": []string{"x"}, "type": []string{"repositories"}, "p": []string{"zero"}}
|
|
334
|
+
if isSupportedGitHubSearchURLQuery(invalid) {
|
|
335
|
+
t.Fatal("invalid search page accepted")
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func TestGitHubSearchEmptyPageIsExplicit(t *testing.T) {
|
|
340
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
341
|
+
w.Header().Set("Content-Type", "application/json")
|
|
342
|
+
_, _ = io.WriteString(w, `{"total_count":0,"incomplete_results":false,"items":[]}`)
|
|
343
|
+
}))
|
|
344
|
+
defer server.Close()
|
|
345
|
+
out, err := readGitHubSearch(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/search?q=does-not-exist&type=repositories"))
|
|
346
|
+
if err != nil {
|
|
347
|
+
t.Fatal(err)
|
|
348
|
+
}
|
|
349
|
+
if !strings.Contains(out, "_No results returned on this page._") {
|
|
350
|
+
t.Fatalf("empty Search page was not explicit:\n%s", out)
|
|
351
|
+
}
|
|
352
|
+
}
|