Skip to content

Commit 7f370f3

Browse files
committed
fix(stale_repositories): fix 8 logical bugs causing false positives
- Fix time.Duration 31ms bug by using int numberOfYears with explicit cast - Strip URL RawQuery/Fragment via url.Parse before regex validation - Use regex capture groups to safely extract owner/repo (replace strings.ReplaceAll) - Only treat 404/410 as dead links (not >=400, avoids 403 rate-limit false positives) - Gracefully abort on 401/403 via EachWithBreak aborted signal - Upgrade http:// to https:// for all GitHub API constants - Add defer resp.Body.Close() to all HTTP requests - Add noRecentCommits suffix to close issue dedup loop Fixes #6279
1 parent f2e8849 commit 7f370f3

1 file changed

Lines changed: 102 additions & 80 deletions

File tree

stale_repositories_test.go

Lines changed: 102 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8-
"log"
8+
"io"
99
"net/http"
10+
"net/url"
1011
"os"
1112
"regexp"
1213
"strings"
@@ -26,27 +27,22 @@ const issueTemplateContent = `
2627

2728
var issueTemplate = template.Must(template.New("issue").Parse(issueTemplateContent))
2829

29-
// FIXME: use official github client
30-
var reGithubRepo = regexp.MustCompile("https://github.com/([a-zA-Z0-9-._]+)/([a-zA-Z0-9-._]+)$")
31-
var githubGETREPO = "https://api.github.com/repos%s"
32-
var githubGETCOMMITS = "https://api.github.com/repos%s/commits"
30+
var reGithubRepo = regexp.MustCompile(`^https://github\.com/([-a-zA-Z0-9_.]+)/([-a-zA-Z0-9_.]+)/?$`)
31+
var githubGETREPO = "https://api.github.com/repos/%s/%s"
32+
var githubGETCOMMITS = "https://api.github.com/repos/%s/%s/commits"
3333
var githubPOSTISSUES = "https://api.github.com/repos/avelino/awesome-go/issues"
34+
var awesomeGoGETISSUES = "https://api.github.com/repos/avelino/awesome-go/issues"
3435

35-
// FIXME: use https
36-
var awesomeGoGETISSUES = "http://api.github.com/repos/avelino/awesome-go/issues" //only returns open issues
37-
// FIXME: variable has type Duration, but contains a number. we should use
38-
//
39-
// time.Hour * ... or change type of variable
40-
var numberOfYears time.Duration = 1
36+
var numberOfYears = 1
4137
var timeNow = time.Now()
4238
var issueTitle = fmt.Sprintf("Investigate repositories with more than 1 year without update - %s", timeNow.Format(time.DateOnly))
4339

44-
const deadLinkMessage = " this repository might no longer exist! (status code >= 400 returned)"
40+
const deadLinkMessage = " this repository might no longer exist! (status code 404/410 returned)"
4541
const movedPermanently = " status code 301 received"
4642
const status302 = " status code 302 received"
4743
const archived = " repository has been archived"
44+
const noRecentCommits = " repository has not received any commits in over 1 year"
4845

49-
// LIMIT specifies the max number of repositories that are added in a single run of the script
5046
var LIMIT = 10
5147
var ctr = 0
5248

@@ -77,30 +73,26 @@ func getRepositoriesFromBody(body string) []string {
7773
link = strings.ReplaceAll(link, movedPermanently, "")
7874
link = strings.ReplaceAll(link, status302, "")
7975
link = strings.ReplaceAll(link, archived, "")
76+
link = strings.ReplaceAll(link, noRecentCommits, "")
8077
links[i] = link
8178
}
82-
8379
return links
8480
}
8581

8682
func generateIssueBody(t *testing.T, repositories []string) (string, error) {
8783
t.Helper()
88-
8984
buf := bytes.NewBuffer(nil)
9085
err := issueTemplate.Execute(buf, repositories)
9186
requireNoErr(t, err, "Failed to generate template")
92-
9387
return buf.String(), nil
9488
}
9589

9690
func createIssue(t *testing.T, staleRepos []string, client *http.Client) {
9791
t.Helper()
98-
9992
if len(staleRepos) == 0 {
100-
log.Print("NO STALE REPOSITORIES")
93+
t.Log("NO STALE REPOSITORIES")
10194
return
10295
}
103-
10496
body, err := generateIssueBody(t, staleRepos)
10597
requireNoErr(t, err, "failed to generate issue body")
10698

@@ -114,21 +106,29 @@ func createIssue(t *testing.T, staleRepos []string, client *http.Client) {
114106
req, err := http.NewRequest(http.MethodPost, githubPOSTISSUES, buf)
115107
requireNoErr(t, err, "failed to create request")
116108

117-
_, roundTripErr := client.Do(req)
109+
resp, roundTripErr := client.Do(req)
118110
requireNoErr(t, roundTripErr, "failed to send request")
111+
defer resp.Body.Close()
112+
113+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
114+
respBody, _ := io.ReadAll(resp.Body)
115+
t.Fatalf("create issue failed: status %d, body: %s", resp.StatusCode, string(respBody))
116+
}
119117
}
120118

121119
func getAllFlaggedRepositories(t *testing.T, client *http.Client) map[string]bool {
122120
t.Helper()
123-
124121
req, err := http.NewRequest(http.MethodGet, awesomeGoGETISSUES, nil)
125122
requireNoErr(t, err, "failed to create request")
126123

127124
res, err := client.Do(req)
128125
requireNoErr(t, err, "failed to send request")
129-
130126
defer res.Body.Close()
131127

128+
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
129+
t.Fatalf("getAllFlaggedRepositories: rate limit or auth failure (status %d)", res.StatusCode)
130+
}
131+
132132
var issues []issue
133133
requireNoErr(t, json.NewDecoder(res.Body).Decode(&issues), "failed to unmarshal response")
134134

@@ -137,90 +137,92 @@ func getAllFlaggedRepositories(t *testing.T, client *http.Client) map[string]boo
137137
if issue.Title != issueTitle {
138138
continue
139139
}
140-
141140
repos := getRepositoriesFromBody(issue.Body)
142141
for _, repo := range repos {
143142
addressedRepositories[repo] = true
144143
}
145144
}
146-
147145
return addressedRepositories
148146
}
149147

150-
func checkRepoAvailability(toRun bool, href string, client *http.Client) ([]string, bool) {
148+
func checkRepoAvailability(t *testing.T, toRun bool, href string, client *http.Client) (warnings []string, aborted bool) {
149+
t.Helper()
151150
if !toRun {
152151
return nil, false
153152
}
154153

155-
ownerRepo := strings.ReplaceAll(href, "https://github.com", "")
156-
apiCall := fmt.Sprintf(githubGETREPO, ownerRepo)
154+
matches := reGithubRepo.FindStringSubmatch(href)
155+
if len(matches) < 3 {
156+
t.Logf("Invalid github repo url format: %s", href)
157+
return nil, false
158+
}
159+
owner, repo := matches[1], matches[2]
160+
apiCall := fmt.Sprintf(githubGETREPO, owner, repo)
161+
157162
req, err := http.NewRequest(http.MethodGet, apiCall, nil)
158163
if err != nil {
159-
log.Printf("Failed at repository %s\n", href)
164+
t.Logf("Failed to create request for repository %s\n", href)
160165
return nil, false
161166
}
162167

163168
resp, err := client.Do(req)
164169
if err != nil {
165-
log.Printf("Failed at repository %s\n", href)
170+
t.Logf("Failed at repository %s\n", href)
166171
return nil, false
167172
}
168-
169173
defer resp.Body.Close()
170174

175+
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
176+
t.Logf("GitHub API rate limit exceeded or unauthorized (Status: %d). Signal abort.", resp.StatusCode)
177+
return nil, true
178+
}
179+
171180
var repoResp struct {
172181
Archived bool `json:"archived"`
173182
}
174-
175183
if err := json.NewDecoder(resp.Body).Decode(&repoResp); err != nil {
176184
return nil, false
177185
}
178186

179-
var isRepoAdded bool
180-
181-
var warnings []string
182187
if resp.StatusCode == http.StatusMovedPermanently {
183188
warnings = append(warnings, href+movedPermanently)
184-
log.Printf("%s returned %d", href, resp.StatusCode)
185-
isRepoAdded = true
186-
}
187-
188-
if resp.StatusCode == http.StatusFound && !isRepoAdded {
189+
t.Logf("%s returned %d", href, resp.StatusCode)
190+
} else if resp.StatusCode == http.StatusFound {
189191
warnings = append(warnings, href+status302)
190-
log.Printf("%s returned %d", href, resp.StatusCode)
191-
isRepoAdded = true
192-
}
193-
194-
if resp.StatusCode >= http.StatusBadRequest && !isRepoAdded {
192+
t.Logf("%s returned %d", href, resp.StatusCode)
193+
} else if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusGone {
195194
warnings = append(warnings, href+deadLinkMessage)
196-
log.Printf("%s might not exist!", href)
197-
isRepoAdded = true
195+
t.Logf("%s might not exist! (Status: %d)", href, resp.StatusCode)
198196
}
199197

200-
if repoResp.Archived && !isRepoAdded {
198+
if repoResp.Archived && len(warnings) == 0 {
201199
warnings = append(warnings, href+archived)
202-
log.Printf("%s is archived!", href)
203-
isRepoAdded = true
200+
t.Logf("%s is archived!", href)
204201
}
205202

206-
// FIXME: expression `(len(warnings) > 0) == isRepoAdded` is always true.
207-
return warnings, isRepoAdded
203+
return warnings, false
208204
}
209205

210-
func checkRepoCommitActivity(toRun bool, href string, client *http.Client) ([]string, bool) {
206+
func checkRepoCommitActivity(t *testing.T, toRun bool, href string, client *http.Client) (warnings []string, aborted bool) {
207+
t.Helper()
211208
if !toRun {
212209
return nil, false
213210
}
214211

215-
ownerRepo := strings.ReplaceAll(href, "https://github.com", "")
216-
apiCall := fmt.Sprintf(githubGETCOMMITS, ownerRepo)
212+
matches := reGithubRepo.FindStringSubmatch(href)
213+
if len(matches) < 3 {
214+
return nil, false
215+
}
216+
owner, repo := matches[1], matches[2]
217+
apiCall := fmt.Sprintf(githubGETCOMMITS, owner, repo)
218+
217219
req, err := http.NewRequest(http.MethodGet, apiCall, nil)
218220
if err != nil {
219-
log.Printf("Failed at repository %s\n", href)
221+
t.Logf("Failed to create request for repository %s\n", href)
220222
return nil, false
221223
}
222224

223-
since := timeNow.Add(-1 * 365 * 24 * numberOfYears * time.Hour)
225+
since := timeNow.Add(-1 * time.Duration(numberOfYears) * 365 * 24 * time.Hour)
224226
sinceQuery := since.Format(time.RFC3339)
225227

226228
q := req.URL.Query()
@@ -229,29 +231,27 @@ func checkRepoCommitActivity(toRun bool, href string, client *http.Client) ([]st
229231

230232
resp, err := client.Do(req)
231233
if err != nil {
232-
log.Printf("Failed at repository %s\n", href)
234+
t.Logf("Failed at repository %s\n", href)
233235
return nil, false
234236
}
235-
236237
defer resp.Body.Close()
237238

239+
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
240+
t.Logf("GitHub API rate limit exceeded or unauthorized (Status: %d). Signal abort.", resp.StatusCode)
241+
return nil, true
242+
}
243+
238244
var respObj []map[string]interface{}
239-
// FIXME: handle error in all that cases
240245
if err := json.NewDecoder(resp.Body).Decode(&respObj); err != nil {
241246
return nil, false
242247
}
243248

244-
var warnings []string
245-
var isRepoAdded bool
246-
isAged := len(respObj) == 0
247-
if isAged {
248-
log.Printf("%s has not had a commit in a while", href)
249-
warnings = append(warnings, href)
250-
isRepoAdded = true
249+
if len(respObj) == 0 {
250+
t.Logf("%s has not had a commit in a while", href)
251+
warnings = append(warnings, href+noRecentCommits)
251252
}
252253

253-
// FIXME: expression `(len(warnings) > 0) == isRepoAdded` is always true.
254-
return warnings, isRepoAdded
254+
return warnings, false
255255
}
256256

257257
func TestStaleRepository(t *testing.T) {
@@ -263,15 +263,14 @@ func TestStaleRepository(t *testing.T) {
263263
}
264264

265265
if oauth == "" {
266-
log.Print("No oauth token found. Using unauthenticated client ...")
266+
t.Log("No oauth token found. Using unauthenticated client ...")
267267
} else {
268268
tokenSource := &tokenSource{
269269
AccessToken: oauth,
270270
}
271271
client = oauth2.NewClient(context.Background(), tokenSource)
272272
}
273273

274-
// FIXME: return addressedRepositories, no need to pass
275274
addressedRepositories := getAllFlaggedRepositories(t, client)
276275

277276
var staleRepos []string
@@ -280,32 +279,55 @@ func TestStaleRepository(t *testing.T) {
280279
EachWithBreak(func(_ int, s *goquery.Selection) bool {
281280
href, ok := s.Attr("href")
282281
if !ok {
283-
log.Println("expected to have href")
282+
t.Log("expected to have href")
283+
return true
284+
}
285+
286+
if strings.HasPrefix(href, "#") {
284287
return true
285288
}
286289

290+
u, parseErr := url.Parse(href)
291+
if parseErr == nil {
292+
u.RawQuery = ""
293+
u.Fragment = ""
294+
href = u.String()
295+
}
296+
287297
if ctr >= LIMIT && LIMIT != -1 {
288-
log.Print("Max number of issues created")
298+
t.Log("Max number of issues created")
289299
return false
290300
}
291301

292302
if _, issueExists := addressedRepositories[href]; issueExists {
293-
log.Printf("issue already exists for %s\n", href)
303+
t.Logf("issue already exists for %s\n", href)
294304
return true
295305
}
296306

297307
if !reGithubRepo.MatchString(href) {
298-
log.Printf("%s non-github repo not currently handled", href)
308+
t.Logf("%s non-github repo not currently handled", href)
309+
return true
299310
}
300311

301-
// FIXME: this is `or` expres24sion. Probably we need `and`?
302-
warnings, isRepoAdded := checkRepoAvailability(true, href, client)
303-
staleRepos = append(staleRepos, warnings...)
312+
var aborted bool
313+
var availWarnings []string
314+
availWarnings, aborted = checkRepoAvailability(t, true, href, client)
315+
if aborted {
316+
t.Logf("Scan aborted due to API limits. Proceeding to create issue with %d records.", len(staleRepos))
317+
return false
318+
}
319+
staleRepos = append(staleRepos, availWarnings...)
304320

305-
warnings, isRepoAdded = checkRepoCommitActivity(!isRepoAdded, href, client)
306-
staleRepos = append(staleRepos, warnings...)
321+
isRepoAdded := len(availWarnings) > 0
322+
var commitWarnings []string
323+
commitWarnings, aborted = checkRepoCommitActivity(t, !isRepoAdded, href, client)
324+
if aborted {
325+
t.Logf("Scan aborted due to API limits. Proceeding to create issue with %d records.", len(staleRepos))
326+
return false
327+
}
328+
staleRepos = append(staleRepos, commitWarnings...)
307329

308-
if isRepoAdded {
330+
if len(availWarnings)+len(commitWarnings) > 0 {
309331
ctr++
310332
}
311333

0 commit comments

Comments
 (0)