fix(domain): reject dash-led SSH users and hosts, check cron gaps on non-leap years and inject the URL validator
- commit
- 5e1949f5e44f48fbc18ada70610846609e24bb85
- author
- brice · · 2026-10-20 07:06 UTC
- committer
- brice · · 2026-10-20 10:02 UTC
- parent
- 335d134
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +165 -89
M internal/domain/event.go +0 -1
| @@ -310,7 +310,6 @@ type MirrorFailed struct { | |||
| 310 | 310 | Repo RepoName | |
| 311 | 311 | State MirrorState | |
| 312 | 312 | Class MirrorFailureClass | |
| 313 | - | Reason string | |
| 314 | 313 | At time.Time | |
| 315 | 314 | Visibility Visibility | |
| 316 | 315 | CreatedAt time.Time | |
| @@ -310,7 +310,6 @@ type MirrorFailed struct { | |||||
| 310 | Repo RepoName | 310 | Repo RepoName | ||
| 311 | State MirrorState | 311 | State MirrorState | ||
| 312 | Class MirrorFailureClass | 312 | Class MirrorFailureClass | ||
| 313 | - | Reason string | |||
| 314 | At time.Time | 313 | At time.Time | ||
| 315 | Visibility Visibility | 314 | Visibility Visibility | ||
| 316 | CreatedAt time.Time | 315 | CreatedAt time.Time | ||
M internal/domain/mirror.go +21 -5
| @@ -72,7 +72,7 @@ func mirrorURLParts(s string) (host, path string, err error) { | |||
| 72 | 72 | if _, ok := u.User.Password(); ok { | |
| 73 | 73 | return bad() | |
| 74 | 74 | } | |
| 75 | - | if u.User.Username() == "" { | |
| 75 | + | if u.User.Username() == "" || strings.HasPrefix(u.User.Username(), "-") { | |
| 76 | 76 | return bad() | |
| 77 | 77 | } | |
| 78 | 78 | } | |
| @@ -95,7 +95,7 @@ func mirrorURLParts(s string) (host, path string, err error) { | |||
| 95 | 95 | } | |
| 96 | 96 | if strings.Contains(host, "@") { | |
| 97 | 97 | user, h, _ := strings.Cut(host, "@") | |
| 98 | - | if user == "" || strings.ContainsAny(user, ":/@") { | |
| 98 | + | if user == "" || strings.HasPrefix(user, "-") || strings.ContainsAny(user, ":/@") { | |
| 99 | 99 | return bad() | |
| 100 | 100 | } | |
| 101 | 101 | host = h | |
| @@ -156,12 +156,18 @@ func mirrorCron(spec string) (Cron, error) { | |||
| 156 | 156 | // "url_template", "cron", "url": cron parsed with ParseCron and checked by CronMinGap >= MirrorMinInterval; | |
| 157 | 157 | // url_template must contain the placeholder; when Enabled both are required. | |
| 158 | 158 | func ValidateMirrorGlobalConfig(g MirrorGlobalConfig) error { | |
| 159 | + | return ValidateMirrorGlobalConfigWith(g, ValidateMirrorURL) | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // ValidateMirrorGlobalConfigWith is ValidateMirrorGlobalConfig with another URL check, for | |
| 163 | + | // deployments and tests that accept more than SSH URLs. | |
| 164 | + | func ValidateMirrorGlobalConfigWith(g MirrorGlobalConfig, validateURL func(string) error) error { | |
| 159 | 165 | var fields []FieldViolation | |
| 160 | 166 | if g.Enabled || g.URLTemplate != "" { | |
| 161 | 167 | repo, _ := ParseRepoName("repo") | |
| 162 | 168 | u, e := ExpandMirrorURL(g.URLTemplate, repo) | |
| 163 | 169 | if e == nil { | |
| 164 | - | e = ValidateMirrorURL(u) | |
| 170 | + | e = validateURL(u) | |
| 165 | 171 | } | |
| 166 | 172 | if e != nil { | |
| 167 | 173 | fields = append(fields, FieldViolation{"url_template", "must be an SSH URL template containing {repo}"}) | |
| @@ -178,9 +184,14 @@ func ValidateMirrorGlobalConfig(g MirrorGlobalConfig) error { | |||
| 178 | 184 | return nil | |
| 179 | 185 | } | |
| 180 | 186 | func ValidateMirrorRepoConfig(r MirrorRepoConfig) error { | |
| 187 | + | return ValidateMirrorRepoConfigWith(r, ValidateMirrorURL) | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // ValidateMirrorRepoConfigWith is ValidateMirrorRepoConfig with another URL check. | |
| 191 | + | func ValidateMirrorRepoConfigWith(r MirrorRepoConfig, validateURL func(string) error) error { | |
| 181 | 192 | var fields []FieldViolation | |
| 182 | 193 | if r.URL != "" { | |
| 183 | - | if e := ValidateMirrorURL(r.URL); e != nil { | |
| 194 | + | if e := validateURL(r.URL); e != nil { | |
| 184 | 195 | fields = append(fields, FieldViolation{"url", e.Error()}) | |
| 185 | 196 | } | |
| 186 | 197 | } | |
| @@ -197,6 +208,11 @@ func ValidateMirrorRepoConfig(r MirrorRepoConfig) error { | |||
| 197 | 208 | ||
| 198 | 209 | // ResolveMirror: override wins even when global is disabled; else global, unless excluded. | |
| 199 | 210 | func ResolveMirror(g MirrorGlobalConfig, r *MirrorRepoConfig, repo RepoName) (MirrorTarget, error) { | |
| 211 | + | return ResolveMirrorWith(g, r, repo, ValidateMirrorURL) | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // ResolveMirrorWith is ResolveMirror with another URL check. | |
| 215 | + | func ResolveMirrorWith(g MirrorGlobalConfig, r *MirrorRepoConfig, repo RepoName, validateURL func(string) error) (MirrorTarget, error) { | |
| 200 | 216 | t := MirrorTarget{Repo: repo, Source: MirrorSourceNone} | |
| 201 | 217 | if r != nil && r.URL != "" { | |
| 202 | 218 | t.URL = r.URL | |
| @@ -224,7 +240,7 @@ func ResolveMirror(g MirrorGlobalConfig, r *MirrorRepoConfig, repo RepoName) (Mi | |||
| 224 | 240 | t.AllBranches = *r.AllBranches | |
| 225 | 241 | } | |
| 226 | 242 | } | |
| 227 | - | if e := ValidateMirrorURL(t.URL); e != nil { | |
| 243 | + | if e := validateURL(t.URL); e != nil { | |
| 228 | 244 | return t, e | |
| 229 | 245 | } | |
| 230 | 246 | // Mutation boundaries validate the minimum gap; resolution only parses. | |
| @@ -72,7 +72,7 @@ func mirrorURLParts(s string) (host, path string, err error) { | |||||
| 72 | if _, ok := u.User.Password(); ok { | 72 | if _, ok := u.User.Password(); ok { | ||
| 73 | return bad() | 73 | return bad() | ||
| 74 | } | 74 | } | ||
| 75 | - | if u.User.Username() == "" { | 75 | + | if u.User.Username() == "" || strings.HasPrefix(u.User.Username(), "-") { |
| 76 | return bad() | 76 | return bad() | ||
| 77 | } | 77 | } | ||
| 78 | } | 78 | } | ||
| @@ -95,7 +95,7 @@ func mirrorURLParts(s string) (host, path string, err error) { | |||||
| 95 | } | 95 | } | ||
| 96 | if strings.Contains(host, "@") { | 96 | if strings.Contains(host, "@") { | ||
| 97 | user, h, _ := strings.Cut(host, "@") | 97 | user, h, _ := strings.Cut(host, "@") | ||
| 98 | - | if user == "" || strings.ContainsAny(user, ":/@") { | 98 | + | if user == "" || strings.HasPrefix(user, "-") || strings.ContainsAny(user, ":/@") { |
| 99 | return bad() | 99 | return bad() | ||
| 100 | } | 100 | } | ||
| 101 | host = h | 101 | host = h | ||
| @@ -156,12 +156,18 @@ func mirrorCron(spec string) (Cron, error) { | |||||
| 156 | // "url_template", "cron", "url": cron parsed with ParseCron and checked by CronMinGap >= MirrorMinInterval; | 156 | // "url_template", "cron", "url": cron parsed with ParseCron and checked by CronMinGap >= MirrorMinInterval; | ||
| 157 | // url_template must contain the placeholder; when Enabled both are required. | 157 | // url_template must contain the placeholder; when Enabled both are required. | ||
| 158 | func ValidateMirrorGlobalConfig(g MirrorGlobalConfig) error { | 158 | func ValidateMirrorGlobalConfig(g MirrorGlobalConfig) error { | ||
| 159 | + | return ValidateMirrorGlobalConfigWith(g, ValidateMirrorURL) | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // ValidateMirrorGlobalConfigWith is ValidateMirrorGlobalConfig with another URL check, for | |||
| 163 | + | // deployments and tests that accept more than SSH URLs. | |||
| 164 | + | func ValidateMirrorGlobalConfigWith(g MirrorGlobalConfig, validateURL func(string) error) error { | |||
| 159 | var fields []FieldViolation | 165 | var fields []FieldViolation | ||
| 160 | if g.Enabled || g.URLTemplate != "" { | 166 | if g.Enabled || g.URLTemplate != "" { | ||
| 161 | repo, _ := ParseRepoName("repo") | 167 | repo, _ := ParseRepoName("repo") | ||
| 162 | u, e := ExpandMirrorURL(g.URLTemplate, repo) | 168 | u, e := ExpandMirrorURL(g.URLTemplate, repo) | ||
| 163 | if e == nil { | 169 | if e == nil { | ||
| 164 | - | e = ValidateMirrorURL(u) | 170 | + | e = validateURL(u) |
| 165 | } | 171 | } | ||
| 166 | if e != nil { | 172 | if e != nil { | ||
| 167 | fields = append(fields, FieldViolation{"url_template", "must be an SSH URL template containing {repo}"}) | 173 | fields = append(fields, FieldViolation{"url_template", "must be an SSH URL template containing {repo}"}) | ||
| @@ -178,9 +184,14 @@ func ValidateMirrorGlobalConfig(g MirrorGlobalConfig) error { | |||||
| 178 | return nil | 184 | return nil | ||
| 179 | } | 185 | } | ||
| 180 | func ValidateMirrorRepoConfig(r MirrorRepoConfig) error { | 186 | func ValidateMirrorRepoConfig(r MirrorRepoConfig) error { | ||
| 187 | + | return ValidateMirrorRepoConfigWith(r, ValidateMirrorURL) | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // ValidateMirrorRepoConfigWith is ValidateMirrorRepoConfig with another URL check. | |||
| 191 | + | func ValidateMirrorRepoConfigWith(r MirrorRepoConfig, validateURL func(string) error) error { | |||
| 181 | var fields []FieldViolation | 192 | var fields []FieldViolation | ||
| 182 | if r.URL != "" { | 193 | if r.URL != "" { | ||
| 183 | - | if e := ValidateMirrorURL(r.URL); e != nil { | 194 | + | if e := validateURL(r.URL); e != nil { |
| 184 | fields = append(fields, FieldViolation{"url", e.Error()}) | 195 | fields = append(fields, FieldViolation{"url", e.Error()}) | ||
| 185 | } | 196 | } | ||
| 186 | } | 197 | } | ||
| @@ -197,6 +208,11 @@ func ValidateMirrorRepoConfig(r MirrorRepoConfig) error { | |||||
| 197 | 208 | ||||
| 198 | // ResolveMirror: override wins even when global is disabled; else global, unless excluded. | 209 | // ResolveMirror: override wins even when global is disabled; else global, unless excluded. | ||
| 199 | func ResolveMirror(g MirrorGlobalConfig, r *MirrorRepoConfig, repo RepoName) (MirrorTarget, error) { | 210 | func ResolveMirror(g MirrorGlobalConfig, r *MirrorRepoConfig, repo RepoName) (MirrorTarget, error) { | ||
| 211 | + | return ResolveMirrorWith(g, r, repo, ValidateMirrorURL) | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | // ResolveMirrorWith is ResolveMirror with another URL check. | |||
| 215 | + | func ResolveMirrorWith(g MirrorGlobalConfig, r *MirrorRepoConfig, repo RepoName, validateURL func(string) error) (MirrorTarget, error) { | |||
| 200 | t := MirrorTarget{Repo: repo, Source: MirrorSourceNone} | 216 | t := MirrorTarget{Repo: repo, Source: MirrorSourceNone} | ||
| 201 | if r != nil && r.URL != "" { | 217 | if r != nil && r.URL != "" { | ||
| 202 | t.URL = r.URL | 218 | t.URL = r.URL | ||
| @@ -224,7 +240,7 @@ func ResolveMirror(g MirrorGlobalConfig, r *MirrorRepoConfig, repo RepoName) (Mi | |||||
| 224 | t.AllBranches = *r.AllBranches | 240 | t.AllBranches = *r.AllBranches | ||
| 225 | } | 241 | } | ||
| 226 | } | 242 | } | ||
| 227 | - | if e := ValidateMirrorURL(t.URL); e != nil { | 243 | + | if e := validateURL(t.URL); e != nil { |
| 228 | return t, e | 244 | return t, e | ||
| 229 | } | 245 | } | ||
| 230 | // Mutation boundaries validate the minimum gap; resolution only parses. | 246 | // Mutation boundaries validate the minimum gap; resolution only parses. | ||
M internal/domain/mirror_cron.go +63 -5
| @@ -1,15 +1,49 @@ | |||
| 1 | 1 | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | 2 | package domain | |
| 3 | 3 | ||
| 4 | - | import "time" | |
| 4 | + | import ( | |
| 5 | + | "sync" | |
| 6 | + | "time" | |
| 7 | + | ) | |
| 5 | 8 | ||
| 9 | + | // cronGapCache memoizes CronMinGap per parsed entry: a scan covers two years of minutes. | |
| 10 | + | const cronGapCacheMax = 256 | |
| 11 | + | ||
| 12 | + | var cronGapCache = struct { | |
| 13 | + | sync.Mutex | |
| 14 | + | m map[Cron]time.Duration | |
| 15 | + | }{m: make(map[Cron]time.Duration)} | |
| 16 | + | ||
| 17 | + | // CronMinGap is the smallest gap between two firings, 0 when the entry fires fewer than twice. | |
| 18 | + | // It scans a leap year, the following non-leap year and the January after, so that Feb 29, | |
| 19 | + | // year-end and Feb 28/Mar 1 neighbours are all compared. | |
| 6 | 20 | func CronMinGap(c Cron) time.Duration { | |
| 7 | - | start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) | |
| 8 | - | // Include the next January so adjacent December/January runs are compared. | |
| 9 | - | end := start.AddDate(1, 1, 0) | |
| 21 | + | cronGapCache.Lock() | |
| 22 | + | gap, ok := cronGapCache.m[c] | |
| 23 | + | cronGapCache.Unlock() | |
| 24 | + | if ok { | |
| 25 | + | return gap | |
| 26 | + | } | |
| 27 | + | gap = scanCronMinGap(c) | |
| 28 | + | cronGapCache.Lock() | |
| 29 | + | if len(cronGapCache.m) >= cronGapCacheMax { | |
| 30 | + | clear(cronGapCache.m) | |
| 31 | + | } | |
| 32 | + | cronGapCache.m[c] = gap | |
| 33 | + | cronGapCache.Unlock() | |
| 34 | + | return gap | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func scanCronMinGap(c Cron) time.Duration { | |
| 38 | + | start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) // leap | |
| 39 | + | end := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC) // 2025 is not leap; include Jan 2026 | |
| 10 | 40 | var prev time.Time | |
| 11 | 41 | var gap time.Duration | |
| 12 | 42 | for t := start; t.Before(end); t = t.Add(time.Minute) { | |
| 43 | + | if !c.dayMatches(t) { | |
| 44 | + | t = t.Truncate(24 * time.Hour).Add(24*time.Hour - time.Minute) | |
| 45 | + | continue | |
| 46 | + | } | |
| 13 | 47 | if c.Matches(t) { | |
| 14 | 48 | if !prev.IsZero() { | |
| 15 | 49 | d := t.Sub(prev) | |
| @@ -22,9 +56,33 @@ func CronMinGap(c Cron) time.Duration { | |||
| 22 | 56 | } | |
| 23 | 57 | return gap | |
| 24 | 58 | } | |
| 59 | + | ||
| 60 | + | // dayMatches is the day-level part of Matches (month, day of month, day of week), UTC. | |
| 61 | + | func (c Cron) dayMatches(t time.Time) bool { | |
| 62 | + | t = t.UTC() | |
| 63 | + | if c.month&(1<<uint(t.Month())) == 0 { | |
| 64 | + | return false | |
| 65 | + | } | |
| 66 | + | domOK := c.dom&(1<<uint(t.Day())) != 0 | |
| 67 | + | dowOK := c.dow&(1<<uint(t.Weekday())) != 0 | |
| 68 | + | if !c.domStar && !c.dowStar { | |
| 69 | + | return domOK || dowOK | |
| 70 | + | } | |
| 71 | + | return domOK && dowOK | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // nextCronHorizon bounds NextCron: a year of minutes. | |
| 75 | + | const nextCronHorizon = 366 * 24 * time.Hour | |
| 76 | + | ||
| 77 | + | // NextCron is the first firing after t, within a year. UTC schedules skip the days that cannot match. | |
| 25 | 78 | func NextCron(s CronSchedule, t time.Time) (time.Time, bool) { | |
| 26 | - | end := t.Add(400 * 24 * time.Hour) | |
| 79 | + | end := t.Add(nextCronHorizon) | |
| 80 | + | utc := s.Loc == nil || s.Loc == time.UTC | |
| 27 | 81 | for n := t.Truncate(time.Minute).Add(time.Minute); !n.After(end); n = n.Add(time.Minute) { | |
| 82 | + | if utc && !s.Cron.dayMatches(n) { | |
| 83 | + | n = n.Truncate(24 * time.Hour).Add(24*time.Hour - time.Minute) | |
| 84 | + | continue | |
| 85 | + | } | |
| 28 | 86 | if s.Due(n) { | |
| 29 | 87 | return n, true | |
| 30 | 88 | } | |
| @@ -1,15 +1,49 @@ | |||||
| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later | 1 | // SPDX-License-Identifier: AGPL-3.0-or-later | ||
| 2 | package domain | 2 | package domain | ||
| 3 | 3 | ||||
| 4 | - | import "time" | 4 | + | import ( |
| 5 | + | "sync" | |||
| 6 | + | "time" | |||
| 7 | + | ) | |||
| 5 | 8 | ||||
| 9 | + | // cronGapCache memoizes CronMinGap per parsed entry: a scan covers two years of minutes. | |||
| 10 | + | const cronGapCacheMax = 256 | |||
| 11 | + | ||||
| 12 | + | var cronGapCache = struct { | |||
| 13 | + | sync.Mutex | |||
| 14 | + | m map[Cron]time.Duration | |||
| 15 | + | }{m: make(map[Cron]time.Duration)} | |||
| 16 | + | ||||
| 17 | + | // CronMinGap is the smallest gap between two firings, 0 when the entry fires fewer than twice. | |||
| 18 | + | // It scans a leap year, the following non-leap year and the January after, so that Feb 29, | |||
| 19 | + | // year-end and Feb 28/Mar 1 neighbours are all compared. | |||
| 6 | func CronMinGap(c Cron) time.Duration { | 20 | func CronMinGap(c Cron) time.Duration { | ||
| 7 | - | start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) | 21 | + | cronGapCache.Lock() |
| 8 | - | // Include the next January so adjacent December/January runs are compared. | 22 | + | gap, ok := cronGapCache.m[c] |
| 9 | - | end := start.AddDate(1, 1, 0) | 23 | + | cronGapCache.Unlock() |
| 24 | + | if ok { | |||
| 25 | + | return gap | |||
| 26 | + | } | |||
| 27 | + | gap = scanCronMinGap(c) | |||
| 28 | + | cronGapCache.Lock() | |||
| 29 | + | if len(cronGapCache.m) >= cronGapCacheMax { | |||
| 30 | + | clear(cronGapCache.m) | |||
| 31 | + | } | |||
| 32 | + | cronGapCache.m[c] = gap | |||
| 33 | + | cronGapCache.Unlock() | |||
| 34 | + | return gap | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func scanCronMinGap(c Cron) time.Duration { | |||
| 38 | + | start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) // leap | |||
| 39 | + | end := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC) // 2025 is not leap; include Jan 2026 | |||
| 10 | var prev time.Time | 40 | var prev time.Time | ||
| 11 | var gap time.Duration | 41 | var gap time.Duration | ||
| 12 | for t := start; t.Before(end); t = t.Add(time.Minute) { | 42 | for t := start; t.Before(end); t = t.Add(time.Minute) { | ||
| 43 | + | if !c.dayMatches(t) { | |||
| 44 | + | t = t.Truncate(24 * time.Hour).Add(24*time.Hour - time.Minute) | |||
| 45 | + | continue | |||
| 46 | + | } | |||
| 13 | if c.Matches(t) { | 47 | if c.Matches(t) { | ||
| 14 | if !prev.IsZero() { | 48 | if !prev.IsZero() { | ||
| 15 | d := t.Sub(prev) | 49 | d := t.Sub(prev) | ||
| @@ -22,9 +56,33 @@ func CronMinGap(c Cron) time.Duration { | |||||
| 22 | } | 56 | } | ||
| 23 | return gap | 57 | return gap | ||
| 24 | } | 58 | } | ||
| 59 | + | ||||
| 60 | + | // dayMatches is the day-level part of Matches (month, day of month, day of week), UTC. | |||
| 61 | + | func (c Cron) dayMatches(t time.Time) bool { | |||
| 62 | + | t = t.UTC() | |||
| 63 | + | if c.month&(1<<uint(t.Month())) == 0 { | |||
| 64 | + | return false | |||
| 65 | + | } | |||
| 66 | + | domOK := c.dom&(1<<uint(t.Day())) != 0 | |||
| 67 | + | dowOK := c.dow&(1<<uint(t.Weekday())) != 0 | |||
| 68 | + | if !c.domStar && !c.dowStar { | |||
| 69 | + | return domOK || dowOK | |||
| 70 | + | } | |||
| 71 | + | return domOK && dowOK | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // nextCronHorizon bounds NextCron: a year of minutes. | |||
| 75 | + | const nextCronHorizon = 366 * 24 * time.Hour | |||
| 76 | + | ||||
| 77 | + | // NextCron is the first firing after t, within a year. UTC schedules skip the days that cannot match. | |||
| 25 | func NextCron(s CronSchedule, t time.Time) (time.Time, bool) { | 78 | func NextCron(s CronSchedule, t time.Time) (time.Time, bool) { | ||
| 26 | - | end := t.Add(400 * 24 * time.Hour) | 79 | + | end := t.Add(nextCronHorizon) |
| 80 | + | utc := s.Loc == nil || s.Loc == time.UTC | |||
| 27 | for n := t.Truncate(time.Minute).Add(time.Minute); !n.After(end); n = n.Add(time.Minute) { | 81 | for n := t.Truncate(time.Minute).Add(time.Minute); !n.After(end); n = n.Add(time.Minute) { | ||
| 82 | + | if utc && !s.Cron.dayMatches(n) { | |||
| 83 | + | n = n.Truncate(24 * time.Hour).Add(24*time.Hour - time.Minute) | |||
| 84 | + | continue | |||
| 85 | + | } | |||
| 28 | if s.Due(n) { | 86 | if s.Due(n) { | ||
| 29 | return n, true | 87 | return n, true | ||
| 30 | } | 88 | } | ||
M internal/domain/mirror_test.go +42 -55
| @@ -19,6 +19,7 @@ func TestMirrorURL(t *testing.T) { | |||
| 19 | 19 | }{ | |
| 20 | 20 | {"git@host:acme/repo", true}, {"host:repo", true}, {"ssh://git@host:2222/acme/repo", true}, {"ssh://[::1]/repo", true}, | |
| 21 | 21 | {"https://host/repo", false}, {"file:///repo", false}, {"git@host:-path", false}, {"ssh://host/-path", false}, {":repo", false}, {"ssh:///repo", false}, {"ssh://git:secret@host/repo", false}, {"user:secret@host:repo", false}, {"host:repo\n", false}, {"ssh://host:0/repo", false}, | |
| 22 | + | {"-oProxyCommand=x@h:p", false}, {"-oProxyCommand=x@h:p/q", false}, {"ssh://-ofoo@h/p", false}, {"ssh://-oHost/p", false}, {"-h:repo", false}, {"git@-h:repo", false}, {"ssh://git@-h/repo", false}, | |
| 22 | 23 | } { | |
| 23 | 24 | t.Run(tt.s, func(t *testing.T) { | |
| 24 | 25 | e := ValidateMirrorURL(tt.s) | |
| @@ -79,7 +80,11 @@ func TestMirrorCronValidation(t *testing.T) { | |||
| 79 | 80 | spec string | |
| 80 | 81 | gap time.Duration | |
| 81 | 82 | valid bool | |
| 82 | - | }{{"*/5 * * * *", 5 * time.Minute, true}, {"*/4 * * * *", 4 * time.Minute, false}, {"0 3 * * *", 24 * time.Hour, true}, {"0 0 29 2 *", 0, true}, {"bad", 0, false}} { | |
| 83 | + | }{{"*/5 * * * *", 5 * time.Minute, true}, {"*/4 * * * *", 4 * time.Minute, false}, {"0 3 * * *", 24 * time.Hour, true}, {"0 0 29 2 *", 0, true}, {"bad", 0, false}, | |
| 84 | + | {"0,59 0,23 1,31 1,12 *", time.Minute, false}, // 31 Dec 23:59 and 1 Jan 00:00 | |
| 85 | + | {"0,59 0,23 1,28 2,3 *", time.Minute, false}, // 28 Feb 23:59 and 1 Mar 00:00 of a non-leap year | |
| 86 | + | {"0,59 0,23 29 2 *", 59 * time.Minute, true}, | |
| 87 | + | } { | |
| 83 | 88 | t.Run(tt.spec, func(t *testing.T) { | |
| 84 | 89 | c, e := ParseCron(tt.spec) | |
| 85 | 90 | if e == nil && CronMinGap(c) != tt.gap { | |
| @@ -125,6 +130,22 @@ func TestMirrorNextCron(t *testing.T) { | |||
| 125 | 130 | if _, ok := NextCron(CronSchedule{}, start); ok { | |
| 126 | 131 | t.Fatal("empty cron fired") | |
| 127 | 132 | } | |
| 133 | + | // A wall clock repeated by a fall-back transition fires once. | |
| 134 | + | c, _ = ParseCron("30 1 * * *") | |
| 135 | + | got, ok = NextCron(CronSchedule{Cron: c, Loc: loc}, time.Date(2024, 11, 3, 5, 30, 0, 0, time.UTC)) | |
| 136 | + | if want := time.Date(2024, 11, 4, 1, 30, 0, 0, loc); !ok || !got.Equal(want) { | |
| 137 | + | t.Fatal(got, ok) | |
| 138 | + | } | |
| 139 | + | // A rare entry is found within a year, not beyond. | |
| 140 | + | c, _ = ParseCron("0 0 29 2 *") | |
| 141 | + | got, ok = NextCron(CronSchedule{Cron: c}, time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC)) | |
| 142 | + | if ok { | |
| 143 | + | t.Fatal("Feb 29 is more than a year away", got) | |
| 144 | + | } | |
| 145 | + | got, ok = NextCron(CronSchedule{Cron: c}, time.Date(2027, 3, 1, 0, 0, 0, 0, time.UTC)) | |
| 146 | + | if want := time.Date(2028, 2, 29, 0, 0, 0, 0, time.UTC); !ok || !got.Equal(want) { | |
| 147 | + | t.Fatal(got, ok) | |
| 148 | + | } | |
| 128 | 149 | } | |
| 129 | 150 | func TestMirrorTransitions(t *testing.T) { | |
| 130 | 151 | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) | |
| @@ -192,15 +213,31 @@ func TestMirrorClassify(t *testing.T) { | |||
| 192 | 213 | for _, tt := range []struct { | |
| 193 | 214 | s string | |
| 194 | 215 | c MirrorFailureClass | |
| 195 | - | }{{"non-fast-forward", MirrorClassDiverged}, {"fetch first", MirrorClassDiverged}, {"Permission denied (publickey)", MirrorClassAuthDenied}, {"Repository not found", MirrorClassRemoteNotFound}, {"does not appear to be a git repository", MirrorClassRemoteNotFound}, {"REMOTE HOST IDENTIFICATION HAS CHANGED", MirrorClassHostKey}, {"Host key verification failed", MirrorClassHostKey}, {"timeout", MirrorClassTransient}} { | |
| 196 | - | if c := ClassifyMirrorOutput(tt.s, errors.New("exit"), false); c != tt.c { | |
| 197 | - | t.Fatal(tt, c) | |
| 198 | - | } | |
| 216 | + | }{{"non-fast-forward", MirrorClassDiverged}, {"fetch first", MirrorClassDiverged}, {"Permission denied (publickey)", MirrorClassAuthDenied}, {"Repository not found", MirrorClassRemoteNotFound}, {"does not appear to be a git repository", MirrorClassRemoteNotFound}, {"REMOTE HOST IDENTIFICATION HAS CHANGED", MirrorClassHostKey}, {"Host key verification failed", MirrorClassHostKey}, {"timeout", MirrorClassTransient}, | |
| 217 | + | {"Permission denied (publickey,password).", MirrorClassAuthDenied}, {"Permission denied (password,publickey,keyboard-interactive).", MirrorClassAuthDenied}, | |
| 218 | + | {"ERROR: Permission to x/y.git denied to deploy key", MirrorClassAuthDenied}, {"remote: Permission to x/y.git denied to someone.", MirrorClassAuthDenied}, | |
| 219 | + | } { | |
| 220 | + | t.Run(tt.s, func(t *testing.T) { | |
| 221 | + | if c := ClassifyMirrorOutput(tt.s, errors.New("exit"), false); c != tt.c { | |
| 222 | + | t.Fatal(c) | |
| 223 | + | } | |
| 224 | + | }) | |
| 199 | 225 | } | |
| 200 | 226 | if ClassifyMirrorOutput("", nil, false) != MirrorClassNone || ClassifyMirrorOutput("non-fast-forward", nil, true) != MirrorClassTransient { | |
| 201 | 227 | t.Fatal("classification") | |
| 202 | 228 | } | |
| 203 | 229 | } | |
| 230 | + | func TestMirrorRecoveryFromAnyState(t *testing.T) { | |
| 231 | + | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) | |
| 232 | + | for _, state := range []MirrorState{MirrorDisabled, MirrorOK, MirrorFailing, MirrorPaused} { | |
| 233 | + | for _, noop := range []bool{false, true} { | |
| 234 | + | tr := ApplyMirrorOutcome(MirrorStatus{State: state, ConsecutiveFailures: 2, Class: MirrorClassTransient, Detail: "old"}, MirrorOutcome{NoOp: noop}, now) | |
| 235 | + | if tr.Recovered != (state == MirrorFailing || state == MirrorPaused) || tr.FailedEvent || tr.Status.ConsecutiveFailures != 0 || tr.Status.Class != "" || tr.Status.Reason != "" || !tr.Status.NextRetryAt.IsZero() { | |
| 236 | + | t.Fatal(state, tr) | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | } | |
| 240 | + | } | |
| 204 | 241 | func TestMirrorRefs(t *testing.T) { | |
| 205 | 242 | id, _ := ParseObjectID(strings.Repeat("a", 40)) | |
| 206 | 243 | refs := []Ref{{Name: "HEAD", ID: id}, {Name: "refs/heads/main", ID: id}, {Name: "refs/heads/other", ID: id}, {Name: "refs/tags/v1", ID: id}, {Name: "refs/pull/1", ID: id}, {Name: "refs/heads/unborn"}} | |
| @@ -253,43 +290,6 @@ func TestMirrorKey(t *testing.T) { | |||
| 253 | 290 | } | |
| 254 | 291 | } | |
| 255 | 292 | ||
| 256 | - | func TestMirrorRecoveryFromFailing(t *testing.T) { | |
| 257 | - | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) | |
| 258 | - | for _, state := range []MirrorState{MirrorDisabled, MirrorOK, MirrorFailing, MirrorPaused} { | |
| 259 | - | for _, noop := range []bool{false, true} { | |
| 260 | - | tr := ApplyMirrorOutcome(MirrorStatus{State: state, ConsecutiveFailures: 2, Class: MirrorClassTransient, Detail: "old"}, MirrorOutcome{NoOp: noop}, now) | |
| 261 | - | if tr.Recovered != (state == MirrorFailing || state == MirrorPaused) || tr.FailedEvent || tr.Status.ConsecutiveFailures != 0 || tr.Status.Class != "" || tr.Status.Reason != "" || !tr.Status.NextRetryAt.IsZero() { | |
| 262 | - | t.Fatal(state, tr) | |
| 263 | - | } | |
| 264 | - | } | |
| 265 | - | } | |
| 266 | - | } | |
| 267 | - | func TestMirrorNextCronFallBack(t *testing.T) { | |
| 268 | - | loc, _ := time.LoadLocation("America/New_York") | |
| 269 | - | c, _ := ParseCron("30 1 * * *") | |
| 270 | - | // After the first 01:30, the repeated 01:30 must not fire again. | |
| 271 | - | start := time.Date(2024, 11, 3, 5, 30, 0, 0, time.UTC) | |
| 272 | - | got, ok := NextCron(CronSchedule{Cron: c, Loc: loc}, start) | |
| 273 | - | want := time.Date(2024, 11, 4, 1, 30, 0, 0, loc) | |
| 274 | - | if !ok || !got.Equal(want) { | |
| 275 | - | t.Fatal(got, ok) | |
| 276 | - | } | |
| 277 | - | } | |
| 278 | - | ||
| 279 | - | func TestMirrorCronYearBoundary(t *testing.T) { | |
| 280 | - | spec := "0,59 0,23 1,31 1,12 *" | |
| 281 | - | c, err := ParseCron(spec) | |
| 282 | - | if err != nil { | |
| 283 | - | t.Fatal(err) | |
| 284 | - | } | |
| 285 | - | if got := CronMinGap(c); got != time.Minute { | |
| 286 | - | t.Fatal(got) | |
| 287 | - | } | |
| 288 | - | if err := ValidateMirrorRepoConfig(MirrorRepoConfig{Cron: spec}); err == nil { | |
| 289 | - | t.Fatal("accepted one-minute year-boundary gap") | |
| 290 | - | } | |
| 291 | - | } | |
| 292 | - | ||
| 293 | 293 | func TestMirrorResolutionOnlyParsesCron(t *testing.T) { | |
| 294 | 294 | g := MirrorGlobalConfig{Enabled: true, URLTemplate: "host:{repo}", Cron: "* * * * *"} | |
| 295 | 295 | repo, _ := ParseRepoName("demo") | |
| @@ -300,16 +300,3 @@ func TestMirrorResolutionOnlyParsesCron(t *testing.T) { | |||
| 300 | 300 | t.Fatal("mutation must still reject short gap") | |
| 301 | 301 | } | |
| 302 | 302 | } | |
| 303 | - | ||
| 304 | - | func TestMirrorClassifyAuthVariants(t *testing.T) { | |
| 305 | - | for _, s := range []string{ | |
| 306 | - | "Permission denied (publickey,password).", | |
| 307 | - | "Permission denied (password,publickey,keyboard-interactive).", | |
| 308 | - | "ERROR: Permission to x/y.git denied to deploy key", | |
| 309 | - | "remote: Permission to x/y.git denied to someone.", | |
| 310 | - | } { | |
| 311 | - | if got := ClassifyMirrorOutput(s, errors.New("exit"), false); got != MirrorClassAuthDenied { | |
| 312 | - | t.Fatalf("%q: %s", s, got) | |
| 313 | - | } | |
| 314 | - | } | |
| 315 | - | } | |
| @@ -19,6 +19,7 @@ func TestMirrorURL(t *testing.T) { | |||||
| 19 | }{ | 19 | }{ | ||
| 20 | {"git@host:acme/repo", true}, {"host:repo", true}, {"ssh://git@host:2222/acme/repo", true}, {"ssh://[::1]/repo", true}, | 20 | {"git@host:acme/repo", true}, {"host:repo", true}, {"ssh://git@host:2222/acme/repo", true}, {"ssh://[::1]/repo", true}, | ||
| 21 | {"https://host/repo", false}, {"file:///repo", false}, {"git@host:-path", false}, {"ssh://host/-path", false}, {":repo", false}, {"ssh:///repo", false}, {"ssh://git:secret@host/repo", false}, {"user:secret@host:repo", false}, {"host:repo\n", false}, {"ssh://host:0/repo", false}, | 21 | {"https://host/repo", false}, {"file:///repo", false}, {"git@host:-path", false}, {"ssh://host/-path", false}, {":repo", false}, {"ssh:///repo", false}, {"ssh://git:secret@host/repo", false}, {"user:secret@host:repo", false}, {"host:repo\n", false}, {"ssh://host:0/repo", false}, | ||
| 22 | + | {"-oProxyCommand=x@h:p", false}, {"-oProxyCommand=x@h:p/q", false}, {"ssh://-ofoo@h/p", false}, {"ssh://-oHost/p", false}, {"-h:repo", false}, {"git@-h:repo", false}, {"ssh://git@-h/repo", false}, | |||
| 22 | } { | 23 | } { | ||
| 23 | t.Run(tt.s, func(t *testing.T) { | 24 | t.Run(tt.s, func(t *testing.T) { | ||
| 24 | e := ValidateMirrorURL(tt.s) | 25 | e := ValidateMirrorURL(tt.s) | ||
| @@ -79,7 +80,11 @@ func TestMirrorCronValidation(t *testing.T) { | |||||
| 79 | spec string | 80 | spec string | ||
| 80 | gap time.Duration | 81 | gap time.Duration | ||
| 81 | valid bool | 82 | valid bool | ||
| 82 | - | }{{"*/5 * * * *", 5 * time.Minute, true}, {"*/4 * * * *", 4 * time.Minute, false}, {"0 3 * * *", 24 * time.Hour, true}, {"0 0 29 2 *", 0, true}, {"bad", 0, false}} { | 83 | + | }{{"*/5 * * * *", 5 * time.Minute, true}, {"*/4 * * * *", 4 * time.Minute, false}, {"0 3 * * *", 24 * time.Hour, true}, {"0 0 29 2 *", 0, true}, {"bad", 0, false}, |
| 84 | + | {"0,59 0,23 1,31 1,12 *", time.Minute, false}, // 31 Dec 23:59 and 1 Jan 00:00 | |||
| 85 | + | {"0,59 0,23 1,28 2,3 *", time.Minute, false}, // 28 Feb 23:59 and 1 Mar 00:00 of a non-leap year | |||
| 86 | + | {"0,59 0,23 29 2 *", 59 * time.Minute, true}, | |||
| 87 | + | } { | |||
| 83 | t.Run(tt.spec, func(t *testing.T) { | 88 | t.Run(tt.spec, func(t *testing.T) { | ||
| 84 | c, e := ParseCron(tt.spec) | 89 | c, e := ParseCron(tt.spec) | ||
| 85 | if e == nil && CronMinGap(c) != tt.gap { | 90 | if e == nil && CronMinGap(c) != tt.gap { | ||
| @@ -125,6 +130,22 @@ func TestMirrorNextCron(t *testing.T) { | |||||
| 125 | if _, ok := NextCron(CronSchedule{}, start); ok { | 130 | if _, ok := NextCron(CronSchedule{}, start); ok { | ||
| 126 | t.Fatal("empty cron fired") | 131 | t.Fatal("empty cron fired") | ||
| 127 | } | 132 | } | ||
| 133 | + | // A wall clock repeated by a fall-back transition fires once. | |||
| 134 | + | c, _ = ParseCron("30 1 * * *") | |||
| 135 | + | got, ok = NextCron(CronSchedule{Cron: c, Loc: loc}, time.Date(2024, 11, 3, 5, 30, 0, 0, time.UTC)) | |||
| 136 | + | if want := time.Date(2024, 11, 4, 1, 30, 0, 0, loc); !ok || !got.Equal(want) { | |||
| 137 | + | t.Fatal(got, ok) | |||
| 138 | + | } | |||
| 139 | + | // A rare entry is found within a year, not beyond. | |||
| 140 | + | c, _ = ParseCron("0 0 29 2 *") | |||
| 141 | + | got, ok = NextCron(CronSchedule{Cron: c}, time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC)) | |||
| 142 | + | if ok { | |||
| 143 | + | t.Fatal("Feb 29 is more than a year away", got) | |||
| 144 | + | } | |||
| 145 | + | got, ok = NextCron(CronSchedule{Cron: c}, time.Date(2027, 3, 1, 0, 0, 0, 0, time.UTC)) | |||
| 146 | + | if want := time.Date(2028, 2, 29, 0, 0, 0, 0, time.UTC); !ok || !got.Equal(want) { | |||
| 147 | + | t.Fatal(got, ok) | |||
| 148 | + | } | |||
| 128 | } | 149 | } | ||
| 129 | func TestMirrorTransitions(t *testing.T) { | 150 | func TestMirrorTransitions(t *testing.T) { | ||
| 130 | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) | 151 | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) | ||
| @@ -192,15 +213,31 @@ func TestMirrorClassify(t *testing.T) { | |||||
| 192 | for _, tt := range []struct { | 213 | for _, tt := range []struct { | ||
| 193 | s string | 214 | s string | ||
| 194 | c MirrorFailureClass | 215 | c MirrorFailureClass | ||
| 195 | - | }{{"non-fast-forward", MirrorClassDiverged}, {"fetch first", MirrorClassDiverged}, {"Permission denied (publickey)", MirrorClassAuthDenied}, {"Repository not found", MirrorClassRemoteNotFound}, {"does not appear to be a git repository", MirrorClassRemoteNotFound}, {"REMOTE HOST IDENTIFICATION HAS CHANGED", MirrorClassHostKey}, {"Host key verification failed", MirrorClassHostKey}, {"timeout", MirrorClassTransient}} { | 216 | + | }{{"non-fast-forward", MirrorClassDiverged}, {"fetch first", MirrorClassDiverged}, {"Permission denied (publickey)", MirrorClassAuthDenied}, {"Repository not found", MirrorClassRemoteNotFound}, {"does not appear to be a git repository", MirrorClassRemoteNotFound}, {"REMOTE HOST IDENTIFICATION HAS CHANGED", MirrorClassHostKey}, {"Host key verification failed", MirrorClassHostKey}, {"timeout", MirrorClassTransient}, |
| 196 | - | if c := ClassifyMirrorOutput(tt.s, errors.New("exit"), false); c != tt.c { | 217 | + | {"Permission denied (publickey,password).", MirrorClassAuthDenied}, {"Permission denied (password,publickey,keyboard-interactive).", MirrorClassAuthDenied}, |
| 197 | - | t.Fatal(tt, c) | 218 | + | {"ERROR: Permission to x/y.git denied to deploy key", MirrorClassAuthDenied}, {"remote: Permission to x/y.git denied to someone.", MirrorClassAuthDenied}, |
| 198 | - | } | 219 | + | } { |
| 220 | + | t.Run(tt.s, func(t *testing.T) { | |||
| 221 | + | if c := ClassifyMirrorOutput(tt.s, errors.New("exit"), false); c != tt.c { | |||
| 222 | + | t.Fatal(c) | |||
| 223 | + | } | |||
| 224 | + | }) | |||
| 199 | } | 225 | } | ||
| 200 | if ClassifyMirrorOutput("", nil, false) != MirrorClassNone || ClassifyMirrorOutput("non-fast-forward", nil, true) != MirrorClassTransient { | 226 | if ClassifyMirrorOutput("", nil, false) != MirrorClassNone || ClassifyMirrorOutput("non-fast-forward", nil, true) != MirrorClassTransient { | ||
| 201 | t.Fatal("classification") | 227 | t.Fatal("classification") | ||
| 202 | } | 228 | } | ||
| 203 | } | 229 | } | ||
| 230 | + | func TestMirrorRecoveryFromAnyState(t *testing.T) { | |||
| 231 | + | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) | |||
| 232 | + | for _, state := range []MirrorState{MirrorDisabled, MirrorOK, MirrorFailing, MirrorPaused} { | |||
| 233 | + | for _, noop := range []bool{false, true} { | |||
| 234 | + | tr := ApplyMirrorOutcome(MirrorStatus{State: state, ConsecutiveFailures: 2, Class: MirrorClassTransient, Detail: "old"}, MirrorOutcome{NoOp: noop}, now) | |||
| 235 | + | if tr.Recovered != (state == MirrorFailing || state == MirrorPaused) || tr.FailedEvent || tr.Status.ConsecutiveFailures != 0 || tr.Status.Class != "" || tr.Status.Reason != "" || !tr.Status.NextRetryAt.IsZero() { | |||
| 236 | + | t.Fatal(state, tr) | |||
| 237 | + | } | |||
| 238 | + | } | |||
| 239 | + | } | |||
| 240 | + | } | |||
| 204 | func TestMirrorRefs(t *testing.T) { | 241 | func TestMirrorRefs(t *testing.T) { | ||
| 205 | id, _ := ParseObjectID(strings.Repeat("a", 40)) | 242 | id, _ := ParseObjectID(strings.Repeat("a", 40)) | ||
| 206 | refs := []Ref{{Name: "HEAD", ID: id}, {Name: "refs/heads/main", ID: id}, {Name: "refs/heads/other", ID: id}, {Name: "refs/tags/v1", ID: id}, {Name: "refs/pull/1", ID: id}, {Name: "refs/heads/unborn"}} | 243 | refs := []Ref{{Name: "HEAD", ID: id}, {Name: "refs/heads/main", ID: id}, {Name: "refs/heads/other", ID: id}, {Name: "refs/tags/v1", ID: id}, {Name: "refs/pull/1", ID: id}, {Name: "refs/heads/unborn"}} | ||
| @@ -253,43 +290,6 @@ func TestMirrorKey(t *testing.T) { | |||||
| 253 | } | 290 | } | ||
| 254 | } | 291 | } | ||
| 255 | 292 | ||||
| 256 | - | func TestMirrorRecoveryFromFailing(t *testing.T) { | |||
| 257 | - | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) | |||
| 258 | - | for _, state := range []MirrorState{MirrorDisabled, MirrorOK, MirrorFailing, MirrorPaused} { | |||
| 259 | - | for _, noop := range []bool{false, true} { | |||
| 260 | - | tr := ApplyMirrorOutcome(MirrorStatus{State: state, ConsecutiveFailures: 2, Class: MirrorClassTransient, Detail: "old"}, MirrorOutcome{NoOp: noop}, now) | |||
| 261 | - | if tr.Recovered != (state == MirrorFailing || state == MirrorPaused) || tr.FailedEvent || tr.Status.ConsecutiveFailures != 0 || tr.Status.Class != "" || tr.Status.Reason != "" || !tr.Status.NextRetryAt.IsZero() { | |||
| 262 | - | t.Fatal(state, tr) | |||
| 263 | - | } | |||
| 264 | - | } | |||
| 265 | - | } | |||
| 266 | - | } | |||
| 267 | - | func TestMirrorNextCronFallBack(t *testing.T) { | |||
| 268 | - | loc, _ := time.LoadLocation("America/New_York") | |||
| 269 | - | c, _ := ParseCron("30 1 * * *") | |||
| 270 | - | // After the first 01:30, the repeated 01:30 must not fire again. | |||
| 271 | - | start := time.Date(2024, 11, 3, 5, 30, 0, 0, time.UTC) | |||
| 272 | - | got, ok := NextCron(CronSchedule{Cron: c, Loc: loc}, start) | |||
| 273 | - | want := time.Date(2024, 11, 4, 1, 30, 0, 0, loc) | |||
| 274 | - | if !ok || !got.Equal(want) { | |||
| 275 | - | t.Fatal(got, ok) | |||
| 276 | - | } | |||
| 277 | - | } | |||
| 278 | - | ||||
| 279 | - | func TestMirrorCronYearBoundary(t *testing.T) { | |||
| 280 | - | spec := "0,59 0,23 1,31 1,12 *" | |||
| 281 | - | c, err := ParseCron(spec) | |||
| 282 | - | if err != nil { | |||
| 283 | - | t.Fatal(err) | |||
| 284 | - | } | |||
| 285 | - | if got := CronMinGap(c); got != time.Minute { | |||
| 286 | - | t.Fatal(got) | |||
| 287 | - | } | |||
| 288 | - | if err := ValidateMirrorRepoConfig(MirrorRepoConfig{Cron: spec}); err == nil { | |||
| 289 | - | t.Fatal("accepted one-minute year-boundary gap") | |||
| 290 | - | } | |||
| 291 | - | } | |||
| 292 | - | ||||
| 293 | func TestMirrorResolutionOnlyParsesCron(t *testing.T) { | 293 | func TestMirrorResolutionOnlyParsesCron(t *testing.T) { | ||
| 294 | g := MirrorGlobalConfig{Enabled: true, URLTemplate: "host:{repo}", Cron: "* * * * *"} | 294 | g := MirrorGlobalConfig{Enabled: true, URLTemplate: "host:{repo}", Cron: "* * * * *"} | ||
| 295 | repo, _ := ParseRepoName("demo") | 295 | repo, _ := ParseRepoName("demo") | ||
| @@ -300,16 +300,3 @@ func TestMirrorResolutionOnlyParsesCron(t *testing.T) { | |||||
| 300 | t.Fatal("mutation must still reject short gap") | 300 | t.Fatal("mutation must still reject short gap") | ||
| 301 | } | 301 | } | ||
| 302 | } | 302 | } | ||
| 303 | - | ||||
| 304 | - | func TestMirrorClassifyAuthVariants(t *testing.T) { | |||
| 305 | - | for _, s := range []string{ | |||
| 306 | - | "Permission denied (publickey,password).", | |||
| 307 | - | "Permission denied (password,publickey,keyboard-interactive).", | |||
| 308 | - | "ERROR: Permission to x/y.git denied to deploy key", | |||
| 309 | - | "remote: Permission to x/y.git denied to someone.", | |||
| 310 | - | } { | |||
| 311 | - | if got := ClassifyMirrorOutput(s, errors.New("exit"), false); got != MirrorClassAuthDenied { | |||
| 312 | - | t.Fatalf("%q: %s", s, got) | |||
| 313 | - | } | |||
| 314 | - | } | |||
| 315 | - | } | |||
M internal/ports/mirror.go +15 -5
| @@ -16,10 +16,10 @@ type MirrorStore interface { | |||
| 16 | 16 | GetRepoConfig(ctx context.Context, repo domain.RepoName) (domain.MirrorRepoConfig, error) // domain.ErrNotFound when none | |
| 17 | 17 | PutRepoConfig(ctx context.Context, c domain.MirrorRepoConfig) error // ErrNotFound when the repository is gone (guardRepoKey) | |
| 18 | 18 | DeleteRepoConfig(ctx context.Context, repo domain.RepoName) error // missing is not an error | |
| 19 | - | ListRepoConfigs(ctx context.Context) ([]domain.MirrorRepoConfig, error) // by repo name | |
| 19 | + | ListRepoConfigs(ctx context.Context) ([]domain.MirrorRepoConfig, error) // by repo name; with ListStatus, what ListStatus of the service reads | |
| 20 | 20 | GetStatus(ctx context.Context, repo domain.RepoName) (domain.MirrorStatus, error) // ErrNotFound when none | |
| 21 | 21 | PutStatus(ctx context.Context, s domain.MirrorStatus) error | |
| 22 | - | ListStatus(ctx context.Context) ([]domain.MirrorStatus, error) | |
| 22 | + | ListStatus(ctx context.Context) ([]domain.MirrorStatus, error) // stored statuses only | |
| 23 | 23 | GetSnapshot(ctx context.Context, repo domain.RepoName) (domain.MirrorSnapshot, error) // ErrNotFound when none | |
| 24 | 24 | PutSnapshot(ctx context.Context, repo domain.RepoName, s domain.MirrorSnapshot) error | |
| 25 | 25 | } | |
| @@ -72,15 +72,24 @@ type MirrorTrigger interface { | |||
| 72 | 72 | ||
| 73 | 73 | // MirrorWarner is what the post-receive hook and the web ask. *usecase.Mirrors implements it. | |
| 74 | 74 | type MirrorWarner interface { | |
| 75 | - | // Warning is non-empty when the mirror of repo is failing or paused ("mirror to github.com:x/y is paused: <reason>"). | |
| 76 | - | Warning(ctx context.Context, repo domain.RepoName) string | |
| 75 | + | // Warning is non-empty when the mirror of repo is failing or paused ("mirror to github.com:x/y is paused: <reason>") | |
| 76 | + | // and p has repo:admin on repo: the remote and the reason are for admins only. | |
| 77 | + | Warning(ctx context.Context, p domain.Principal, repo domain.RepoName) string | |
| 77 | 78 | // Badge is the state and reason for a viewer: reason is "" unless p may administer the repository. | |
| 78 | 79 | Badge(ctx context.Context, p domain.Principal, repo domain.RepoName) (domain.MirrorState, string) | |
| 79 | 80 | } | |
| 80 | 81 | type MirrorSyncResult struct { | |
| 82 | + | Repo domain.RepoName | |
| 81 | 83 | Status domain.MirrorStatus | |
| 82 | 84 | Queued bool | |
| 83 | 85 | NoOp bool | |
| 86 | + | Err error // SyncAll only: why this repository failed; the other repositories still ran | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // MirrorKeyRotation is the outcome of RotateKey; Previous is zero when no key existed. | |
| 90 | + | type MirrorKeyRotation struct { | |
| 91 | + | Key domain.MirrorPublicKey | |
| 92 | + | Previous domain.MirrorPublicKey | |
| 84 | 93 | } | |
| 85 | 94 | ||
| 86 | 95 | // MirrorService is the inbound port of the API. Authorization: key show needs a non-anonymous | |
| @@ -88,7 +97,7 @@ type MirrorSyncResult struct { | |||
| 88 | 97 | // Sync need Allows(ScopeRepoAdmin, repo); GetStatus needs read access to the repository. | |
| 89 | 98 | type MirrorService interface { | |
| 90 | 99 | ShowKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) // generates lazily | |
| 91 | - | RotateKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) | |
| 100 | + | RotateKey(ctx context.Context, p domain.Principal) (MirrorKeyRotation, error) | |
| 92 | 101 | GetGlobalConfig(ctx context.Context, p domain.Principal) (domain.MirrorGlobalConfig, error) | |
| 93 | 102 | SetGlobalConfig(ctx context.Context, p domain.Principal, patch MirrorGlobalPatch) (domain.MirrorGlobalConfig, error) | |
| 94 | 103 | SetRepoMirror(ctx context.Context, p domain.Principal, c domain.MirrorRepoConfig) (domain.MirrorTarget, error) | |
| @@ -97,6 +106,7 @@ type MirrorService interface { | |||
| 97 | 106 | GetStatus(ctx context.Context, p domain.Principal, repo domain.RepoName) (MirrorStatusView, error) | |
| 98 | 107 | ListStatus(ctx context.Context, p domain.Principal) ([]MirrorStatusView, error) | |
| 99 | 108 | Sync(ctx context.Context, p domain.Principal, repo domain.RepoName, force bool) (MirrorSyncResult, error) | |
| 109 | + | // SyncAll skips disabled mirrors and continues past failures, reported per repository in Err. | |
| 100 | 110 | SyncAll(ctx context.Context, p domain.Principal, force bool) ([]MirrorSyncResult, error) | |
| 101 | 111 | } | |
| 102 | 112 | type MirrorGlobalPatch struct { | |
| @@ -16,10 +16,10 @@ type MirrorStore interface { | |||||
| 16 | GetRepoConfig(ctx context.Context, repo domain.RepoName) (domain.MirrorRepoConfig, error) // domain.ErrNotFound when none | 16 | GetRepoConfig(ctx context.Context, repo domain.RepoName) (domain.MirrorRepoConfig, error) // domain.ErrNotFound when none | ||
| 17 | PutRepoConfig(ctx context.Context, c domain.MirrorRepoConfig) error // ErrNotFound when the repository is gone (guardRepoKey) | 17 | PutRepoConfig(ctx context.Context, c domain.MirrorRepoConfig) error // ErrNotFound when the repository is gone (guardRepoKey) | ||
| 18 | DeleteRepoConfig(ctx context.Context, repo domain.RepoName) error // missing is not an error | 18 | DeleteRepoConfig(ctx context.Context, repo domain.RepoName) error // missing is not an error | ||
| 19 | - | ListRepoConfigs(ctx context.Context) ([]domain.MirrorRepoConfig, error) // by repo name | 19 | + | ListRepoConfigs(ctx context.Context) ([]domain.MirrorRepoConfig, error) // by repo name; with ListStatus, what ListStatus of the service reads |
| 20 | GetStatus(ctx context.Context, repo domain.RepoName) (domain.MirrorStatus, error) // ErrNotFound when none | 20 | GetStatus(ctx context.Context, repo domain.RepoName) (domain.MirrorStatus, error) // ErrNotFound when none | ||
| 21 | PutStatus(ctx context.Context, s domain.MirrorStatus) error | 21 | PutStatus(ctx context.Context, s domain.MirrorStatus) error | ||
| 22 | - | ListStatus(ctx context.Context) ([]domain.MirrorStatus, error) | 22 | + | ListStatus(ctx context.Context) ([]domain.MirrorStatus, error) // stored statuses only |
| 23 | GetSnapshot(ctx context.Context, repo domain.RepoName) (domain.MirrorSnapshot, error) // ErrNotFound when none | 23 | GetSnapshot(ctx context.Context, repo domain.RepoName) (domain.MirrorSnapshot, error) // ErrNotFound when none | ||
| 24 | PutSnapshot(ctx context.Context, repo domain.RepoName, s domain.MirrorSnapshot) error | 24 | PutSnapshot(ctx context.Context, repo domain.RepoName, s domain.MirrorSnapshot) error | ||
| 25 | } | 25 | } | ||
| @@ -72,15 +72,24 @@ type MirrorTrigger interface { | |||||
| 72 | 72 | ||||
| 73 | // MirrorWarner is what the post-receive hook and the web ask. *usecase.Mirrors implements it. | 73 | // MirrorWarner is what the post-receive hook and the web ask. *usecase.Mirrors implements it. | ||
| 74 | type MirrorWarner interface { | 74 | type MirrorWarner interface { | ||
| 75 | - | // Warning is non-empty when the mirror of repo is failing or paused ("mirror to github.com:x/y is paused: <reason>"). | 75 | + | // Warning is non-empty when the mirror of repo is failing or paused ("mirror to github.com:x/y is paused: <reason>") |
| 76 | - | Warning(ctx context.Context, repo domain.RepoName) string | 76 | + | // and p has repo:admin on repo: the remote and the reason are for admins only. |
| 77 | + | Warning(ctx context.Context, p domain.Principal, repo domain.RepoName) string | |||
| 77 | // Badge is the state and reason for a viewer: reason is "" unless p may administer the repository. | 78 | // Badge is the state and reason for a viewer: reason is "" unless p may administer the repository. | ||
| 78 | Badge(ctx context.Context, p domain.Principal, repo domain.RepoName) (domain.MirrorState, string) | 79 | Badge(ctx context.Context, p domain.Principal, repo domain.RepoName) (domain.MirrorState, string) | ||
| 79 | } | 80 | } | ||
| 80 | type MirrorSyncResult struct { | 81 | type MirrorSyncResult struct { | ||
| 82 | + | Repo domain.RepoName | |||
| 81 | Status domain.MirrorStatus | 83 | Status domain.MirrorStatus | ||
| 82 | Queued bool | 84 | Queued bool | ||
| 83 | NoOp bool | 85 | NoOp bool | ||
| 86 | + | Err error // SyncAll only: why this repository failed; the other repositories still ran | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // MirrorKeyRotation is the outcome of RotateKey; Previous is zero when no key existed. | |||
| 90 | + | type MirrorKeyRotation struct { | |||
| 91 | + | Key domain.MirrorPublicKey | |||
| 92 | + | Previous domain.MirrorPublicKey | |||
| 84 | } | 93 | } | ||
| 85 | 94 | ||||
| 86 | // MirrorService is the inbound port of the API. Authorization: key show needs a non-anonymous | 95 | // MirrorService is the inbound port of the API. Authorization: key show needs a non-anonymous | ||
| @@ -88,7 +97,7 @@ type MirrorSyncResult struct { | |||||
| 88 | // Sync need Allows(ScopeRepoAdmin, repo); GetStatus needs read access to the repository. | 97 | // Sync need Allows(ScopeRepoAdmin, repo); GetStatus needs read access to the repository. | ||
| 89 | type MirrorService interface { | 98 | type MirrorService interface { | ||
| 90 | ShowKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) // generates lazily | 99 | ShowKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) // generates lazily | ||
| 91 | - | RotateKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) | 100 | + | RotateKey(ctx context.Context, p domain.Principal) (MirrorKeyRotation, error) |
| 92 | GetGlobalConfig(ctx context.Context, p domain.Principal) (domain.MirrorGlobalConfig, error) | 101 | GetGlobalConfig(ctx context.Context, p domain.Principal) (domain.MirrorGlobalConfig, error) | ||
| 93 | SetGlobalConfig(ctx context.Context, p domain.Principal, patch MirrorGlobalPatch) (domain.MirrorGlobalConfig, error) | 102 | SetGlobalConfig(ctx context.Context, p domain.Principal, patch MirrorGlobalPatch) (domain.MirrorGlobalConfig, error) | ||
| 94 | SetRepoMirror(ctx context.Context, p domain.Principal, c domain.MirrorRepoConfig) (domain.MirrorTarget, error) | 103 | SetRepoMirror(ctx context.Context, p domain.Principal, c domain.MirrorRepoConfig) (domain.MirrorTarget, error) | ||
| @@ -97,6 +106,7 @@ type MirrorService interface { | |||||
| 97 | GetStatus(ctx context.Context, p domain.Principal, repo domain.RepoName) (MirrorStatusView, error) | 106 | GetStatus(ctx context.Context, p domain.Principal, repo domain.RepoName) (MirrorStatusView, error) | ||
| 98 | ListStatus(ctx context.Context, p domain.Principal) ([]MirrorStatusView, error) | 107 | ListStatus(ctx context.Context, p domain.Principal) ([]MirrorStatusView, error) | ||
| 99 | Sync(ctx context.Context, p domain.Principal, repo domain.RepoName, force bool) (MirrorSyncResult, error) | 108 | Sync(ctx context.Context, p domain.Principal, repo domain.RepoName, force bool) (MirrorSyncResult, error) | ||
| 109 | + | // SyncAll skips disabled mirrors and continues past failures, reported per repository in Err. | |||
| 100 | SyncAll(ctx context.Context, p domain.Principal, force bool) ([]MirrorSyncResult, error) | 110 | SyncAll(ctx context.Context, p domain.Principal, force bool) ([]MirrorSyncResult, error) | ||
| 101 | } | 111 | } | ||
| 102 | type MirrorGlobalPatch struct { | 112 | type MirrorGlobalPatch struct { | ||
M internal/ports/mocks/MirrorService.go +8 -8
| @@ -318,22 +318,22 @@ func (_c *MirrorService_RemoveRepoMirror_Call) RunAndReturn(run func(ctx context | |||
| 318 | 318 | } | |
| 319 | 319 | ||
| 320 | 320 | // RotateKey provides a mock function for the type MirrorService | |
| 321 | - | func (_mock *MirrorService) RotateKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) { | |
| 321 | + | func (_mock *MirrorService) RotateKey(ctx context.Context, p domain.Principal) (ports.MirrorKeyRotation, error) { | |
| 322 | 322 | ret := _mock.Called(ctx, p) | |
| 323 | 323 | ||
| 324 | 324 | if len(ret) == 0 { | |
| 325 | 325 | panic("no return value specified for RotateKey") | |
| 326 | 326 | } | |
| 327 | 327 | ||
| 328 | - | var r0 domain.MirrorPublicKey | |
| 328 | + | var r0 ports.MirrorKeyRotation | |
| 329 | 329 | var r1 error | |
| 330 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal) (domain.MirrorPublicKey, error)); ok { | |
| 330 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal) (ports.MirrorKeyRotation, error)); ok { | |
| 331 | 331 | return returnFunc(ctx, p) | |
| 332 | 332 | } | |
| 333 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal) domain.MirrorPublicKey); ok { | |
| 333 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal) ports.MirrorKeyRotation); ok { | |
| 334 | 334 | r0 = returnFunc(ctx, p) | |
| 335 | 335 | } else { | |
| 336 | - | r0 = ret.Get(0).(domain.MirrorPublicKey) | |
| 336 | + | r0 = ret.Get(0).(ports.MirrorKeyRotation) | |
| 337 | 337 | } | |
| 338 | 338 | if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal) error); ok { | |
| 339 | 339 | r1 = returnFunc(ctx, p) | |
| @@ -373,12 +373,12 @@ func (_c *MirrorService_RotateKey_Call) Run(run func(ctx context.Context, p doma | |||
| 373 | 373 | return _c | |
| 374 | 374 | } | |
| 375 | 375 | ||
| 376 | - | func (_c *MirrorService_RotateKey_Call) Return(mirrorPublicKey domain.MirrorPublicKey, err error) *MirrorService_RotateKey_Call { | |
| 377 | - | _c.Call.Return(mirrorPublicKey, err) | |
| 376 | + | func (_c *MirrorService_RotateKey_Call) Return(mirrorKeyRotation ports.MirrorKeyRotation, err error) *MirrorService_RotateKey_Call { | |
| 377 | + | _c.Call.Return(mirrorKeyRotation, err) | |
| 378 | 378 | return _c | |
| 379 | 379 | } | |
| 380 | 380 | ||
| 381 | - | func (_c *MirrorService_RotateKey_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error)) *MirrorService_RotateKey_Call { | |
| 381 | + | func (_c *MirrorService_RotateKey_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal) (ports.MirrorKeyRotation, error)) *MirrorService_RotateKey_Call { | |
| 382 | 382 | _c.Call.Return(run) | |
| 383 | 383 | return _c | |
| 384 | 384 | } | |
| @@ -318,22 +318,22 @@ func (_c *MirrorService_RemoveRepoMirror_Call) RunAndReturn(run func(ctx context | |||||
| 318 | } | 318 | } | ||
| 319 | 319 | ||||
| 320 | // RotateKey provides a mock function for the type MirrorService | 320 | // RotateKey provides a mock function for the type MirrorService | ||
| 321 | - | func (_mock *MirrorService) RotateKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) { | 321 | + | func (_mock *MirrorService) RotateKey(ctx context.Context, p domain.Principal) (ports.MirrorKeyRotation, error) { |
| 322 | ret := _mock.Called(ctx, p) | 322 | ret := _mock.Called(ctx, p) | ||
| 323 | 323 | ||||
| 324 | if len(ret) == 0 { | 324 | if len(ret) == 0 { | ||
| 325 | panic("no return value specified for RotateKey") | 325 | panic("no return value specified for RotateKey") | ||
| 326 | } | 326 | } | ||
| 327 | 327 | ||||
| 328 | - | var r0 domain.MirrorPublicKey | 328 | + | var r0 ports.MirrorKeyRotation |
| 329 | var r1 error | 329 | var r1 error | ||
| 330 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal) (domain.MirrorPublicKey, error)); ok { | 330 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal) (ports.MirrorKeyRotation, error)); ok { |
| 331 | return returnFunc(ctx, p) | 331 | return returnFunc(ctx, p) | ||
| 332 | } | 332 | } | ||
| 333 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal) domain.MirrorPublicKey); ok { | 333 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal) ports.MirrorKeyRotation); ok { |
| 334 | r0 = returnFunc(ctx, p) | 334 | r0 = returnFunc(ctx, p) | ||
| 335 | } else { | 335 | } else { | ||
| 336 | - | r0 = ret.Get(0).(domain.MirrorPublicKey) | 336 | + | r0 = ret.Get(0).(ports.MirrorKeyRotation) |
| 337 | } | 337 | } | ||
| 338 | if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal) error); ok { | 338 | if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal) error); ok { | ||
| 339 | r1 = returnFunc(ctx, p) | 339 | r1 = returnFunc(ctx, p) | ||
| @@ -373,12 +373,12 @@ func (_c *MirrorService_RotateKey_Call) Run(run func(ctx context.Context, p doma | |||||
| 373 | return _c | 373 | return _c | ||
| 374 | } | 374 | } | ||
| 375 | 375 | ||||
| 376 | - | func (_c *MirrorService_RotateKey_Call) Return(mirrorPublicKey domain.MirrorPublicKey, err error) *MirrorService_RotateKey_Call { | 376 | + | func (_c *MirrorService_RotateKey_Call) Return(mirrorKeyRotation ports.MirrorKeyRotation, err error) *MirrorService_RotateKey_Call { |
| 377 | - | _c.Call.Return(mirrorPublicKey, err) | 377 | + | _c.Call.Return(mirrorKeyRotation, err) |
| 378 | return _c | 378 | return _c | ||
| 379 | } | 379 | } | ||
| 380 | 380 | ||||
| 381 | - | func (_c *MirrorService_RotateKey_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error)) *MirrorService_RotateKey_Call { | 381 | + | func (_c *MirrorService_RotateKey_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal) (ports.MirrorKeyRotation, error)) *MirrorService_RotateKey_Call { |
| 382 | _c.Call.Return(run) | 382 | _c.Call.Return(run) | ||
| 383 | return _c | 383 | return _c | ||
| 384 | } | 384 | } | ||
M internal/ports/mocks/MirrorWarner.go +16 -10
| @@ -120,16 +120,16 @@ func (_c *MirrorWarner_Badge_Call) RunAndReturn(run func(ctx context.Context, p | |||
| 120 | 120 | } | |
| 121 | 121 | ||
| 122 | 122 | // Warning provides a mock function for the type MirrorWarner | |
| 123 | - | func (_mock *MirrorWarner) Warning(ctx context.Context, repo domain.RepoName) string { | |
| 124 | - | ret := _mock.Called(ctx, repo) | |
| 123 | + | func (_mock *MirrorWarner) Warning(ctx context.Context, p domain.Principal, repo domain.RepoName) string { | |
| 124 | + | ret := _mock.Called(ctx, p, repo) | |
| 125 | 125 | ||
| 126 | 126 | if len(ret) == 0 { | |
| 127 | 127 | panic("no return value specified for Warning") | |
| 128 | 128 | } | |
| 129 | 129 | ||
| 130 | 130 | var r0 string | |
| 131 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) string); ok { | |
| 132 | - | r0 = returnFunc(ctx, repo) | |
| 131 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) string); ok { | |
| 132 | + | r0 = returnFunc(ctx, p, repo) | |
| 133 | 133 | } else { | |
| 134 | 134 | r0 = ret.Get(0).(string) | |
| 135 | 135 | } | |
| @@ -143,24 +143,30 @@ type MirrorWarner_Warning_Call struct { | |||
| 143 | 143 | ||
| 144 | 144 | // Warning is a helper method to define mock.On call | |
| 145 | 145 | // - ctx context.Context | |
| 146 | + | // - p domain.Principal | |
| 146 | 147 | // - repo domain.RepoName | |
| 147 | - | func (_e *MirrorWarner_Expecter) Warning(ctx any, repo any) *MirrorWarner_Warning_Call { | |
| 148 | - | return &MirrorWarner_Warning_Call{Call: _e.mock.On("Warning", ctx, repo)} | |
| 148 | + | func (_e *MirrorWarner_Expecter) Warning(ctx any, p any, repo any) *MirrorWarner_Warning_Call { | |
| 149 | + | return &MirrorWarner_Warning_Call{Call: _e.mock.On("Warning", ctx, p, repo)} | |
| 149 | 150 | } | |
| 150 | 151 | ||
| 151 | - | func (_c *MirrorWarner_Warning_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *MirrorWarner_Warning_Call { | |
| 152 | + | func (_c *MirrorWarner_Warning_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName)) *MirrorWarner_Warning_Call { | |
| 152 | 153 | _c.Call.Run(func(args mock.Arguments) { | |
| 153 | 154 | var arg0 context.Context | |
| 154 | 155 | if args[0] != nil { | |
| 155 | 156 | arg0 = args[0].(context.Context) | |
| 156 | 157 | } | |
| 157 | - | var arg1 domain.RepoName | |
| 158 | + | var arg1 domain.Principal | |
| 158 | 159 | if args[1] != nil { | |
| 159 | - | arg1 = args[1].(domain.RepoName) | |
| 160 | + | arg1 = args[1].(domain.Principal) | |
| 161 | + | } | |
| 162 | + | var arg2 domain.RepoName | |
| 163 | + | if args[2] != nil { | |
| 164 | + | arg2 = args[2].(domain.RepoName) | |
| 160 | 165 | } | |
| 161 | 166 | run( | |
| 162 | 167 | arg0, | |
| 163 | 168 | arg1, | |
| 169 | + | arg2, | |
| 164 | 170 | ) | |
| 165 | 171 | }) | |
| 166 | 172 | return _c | |
| @@ -171,7 +177,7 @@ func (_c *MirrorWarner_Warning_Call) Return(s string) *MirrorWarner_Warning_Call | |||
| 171 | 177 | return _c | |
| 172 | 178 | } | |
| 173 | 179 | ||
| 174 | - | func (_c *MirrorWarner_Warning_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) string) *MirrorWarner_Warning_Call { | |
| 180 | + | func (_c *MirrorWarner_Warning_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName) string) *MirrorWarner_Warning_Call { | |
| 175 | 181 | _c.Call.Return(run) | |
| 176 | 182 | return _c | |
| 177 | 183 | } | |
| @@ -120,16 +120,16 @@ func (_c *MirrorWarner_Badge_Call) RunAndReturn(run func(ctx context.Context, p | |||||
| 120 | } | 120 | } | ||
| 121 | 121 | ||||
| 122 | // Warning provides a mock function for the type MirrorWarner | 122 | // Warning provides a mock function for the type MirrorWarner | ||
| 123 | - | func (_mock *MirrorWarner) Warning(ctx context.Context, repo domain.RepoName) string { | 123 | + | func (_mock *MirrorWarner) Warning(ctx context.Context, p domain.Principal, repo domain.RepoName) string { |
| 124 | - | ret := _mock.Called(ctx, repo) | 124 | + | ret := _mock.Called(ctx, p, repo) |
| 125 | 125 | ||||
| 126 | if len(ret) == 0 { | 126 | if len(ret) == 0 { | ||
| 127 | panic("no return value specified for Warning") | 127 | panic("no return value specified for Warning") | ||
| 128 | } | 128 | } | ||
| 129 | 129 | ||||
| 130 | var r0 string | 130 | var r0 string | ||
| 131 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) string); ok { | 131 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) string); ok { |
| 132 | - | r0 = returnFunc(ctx, repo) | 132 | + | r0 = returnFunc(ctx, p, repo) |
| 133 | } else { | 133 | } else { | ||
| 134 | r0 = ret.Get(0).(string) | 134 | r0 = ret.Get(0).(string) | ||
| 135 | } | 135 | } | ||
| @@ -143,24 +143,30 @@ type MirrorWarner_Warning_Call struct { | |||||
| 143 | 143 | ||||
| 144 | // Warning is a helper method to define mock.On call | 144 | // Warning is a helper method to define mock.On call | ||
| 145 | // - ctx context.Context | 145 | // - ctx context.Context | ||
| 146 | + | // - p domain.Principal | |||
| 146 | // - repo domain.RepoName | 147 | // - repo domain.RepoName | ||
| 147 | - | func (_e *MirrorWarner_Expecter) Warning(ctx any, repo any) *MirrorWarner_Warning_Call { | 148 | + | func (_e *MirrorWarner_Expecter) Warning(ctx any, p any, repo any) *MirrorWarner_Warning_Call { |
| 148 | - | return &MirrorWarner_Warning_Call{Call: _e.mock.On("Warning", ctx, repo)} | 149 | + | return &MirrorWarner_Warning_Call{Call: _e.mock.On("Warning", ctx, p, repo)} |
| 149 | } | 150 | } | ||
| 150 | 151 | ||||
| 151 | - | func (_c *MirrorWarner_Warning_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *MirrorWarner_Warning_Call { | 152 | + | func (_c *MirrorWarner_Warning_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName)) *MirrorWarner_Warning_Call { |
| 152 | _c.Call.Run(func(args mock.Arguments) { | 153 | _c.Call.Run(func(args mock.Arguments) { | ||
| 153 | var arg0 context.Context | 154 | var arg0 context.Context | ||
| 154 | if args[0] != nil { | 155 | if args[0] != nil { | ||
| 155 | arg0 = args[0].(context.Context) | 156 | arg0 = args[0].(context.Context) | ||
| 156 | } | 157 | } | ||
| 157 | - | var arg1 domain.RepoName | 158 | + | var arg1 domain.Principal |
| 158 | if args[1] != nil { | 159 | if args[1] != nil { | ||
| 159 | - | arg1 = args[1].(domain.RepoName) | 160 | + | arg1 = args[1].(domain.Principal) |
| 161 | + | } | |||
| 162 | + | var arg2 domain.RepoName | |||
| 163 | + | if args[2] != nil { | |||
| 164 | + | arg2 = args[2].(domain.RepoName) | |||
| 160 | } | 165 | } | ||
| 161 | run( | 166 | run( | ||
| 162 | arg0, | 167 | arg0, | ||
| 163 | arg1, | 168 | arg1, | ||
| 169 | + | arg2, | |||
| 164 | ) | 170 | ) | ||
| 165 | }) | 171 | }) | ||
| 166 | return _c | 172 | return _c | ||
| @@ -171,7 +177,7 @@ func (_c *MirrorWarner_Warning_Call) Return(s string) *MirrorWarner_Warning_Call | |||||
| 171 | return _c | 177 | return _c | ||
| 172 | } | 178 | } | ||
| 173 | 179 | ||||
| 174 | - | func (_c *MirrorWarner_Warning_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) string) *MirrorWarner_Warning_Call { | 180 | + | func (_c *MirrorWarner_Warning_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName) string) *MirrorWarner_Warning_Call { |
| 175 | _c.Call.Return(run) | 181 | _c.Call.Return(run) | ||
| 176 | return _c | 182 | return _c | ||
| 177 | } | 183 | } | ||