@@ -5,6 +5,7 @@ package repos_test |
| 5 | 5 | import ( |
| 6 | 6 | "context" |
| 7 | 7 | "errors" |
| 8 | + "fmt" |
| 8 | 9 | "io" |
| 9 | 10 | "log/slog" |
| 10 | 11 | "os/exec" |
@@ -265,6 +266,65 @@ func TestCreate_OrgOwned(t *testing.T) { |
| 265 | 266 | } |
| 266 | 267 | } |
| 267 | 268 | |
| 269 | +// TestCreate_ThrottlesNonAdmin saturates the per-actor cap directly via |
| 270 | +// the limiter and confirms a non-admin Create call returns the typed |
| 271 | +// throttle error. Doing it this way avoids spinning up |
| 272 | +// CreateRateLimitMax+1 real repositories. |
| 273 | +func TestCreate_ThrottlesNonAdmin(t *testing.T) { |
| 274 | + t.Parallel() |
| 275 | + _, deps, uid, uname, _ := setupCreateEnv(t) |
| 276 | + saturateCreateLimiter(t, deps, uid) |
| 277 | + |
| 278 | + _, err := repos.Create(context.Background(), deps, repos.Params{ |
| 279 | + OwnerUserID: uid, |
| 280 | + OwnerUsername: uname, |
| 281 | + Name: "should-throttle", |
| 282 | + Visibility: "public", |
| 283 | + }) |
| 284 | + if !throttle.IsThrottled(err) { |
| 285 | + t.Fatalf("Create: err = %v, want throttle error", err) |
| 286 | + } |
| 287 | +} |
| 288 | + |
| 289 | +// TestCreate_SiteAdminBypassesThrottle is the bookend: same saturated |
| 290 | +// counter, but with ActorIsSiteAdmin=true the create succeeds. |
| 291 | +func TestCreate_SiteAdminBypassesThrottle(t *testing.T) { |
| 292 | + t.Parallel() |
| 293 | + _, deps, uid, uname, _ := setupCreateEnv(t) |
| 294 | + saturateCreateLimiter(t, deps, uid) |
| 295 | + |
| 296 | + res, err := repos.Create(context.Background(), deps, repos.Params{ |
| 297 | + OwnerUserID: uid, |
| 298 | + OwnerUsername: uname, |
| 299 | + ActorIsSiteAdmin: true, |
| 300 | + Name: "admin-bypass", |
| 301 | + Visibility: "public", |
| 302 | + }) |
| 303 | + if err != nil { |
| 304 | + t.Fatalf("Create with admin bypass: %v", err) |
| 305 | + } |
| 306 | + if res.Repo.Name != "admin-bypass" { |
| 307 | + t.Fatalf("created repo name = %q, want admin-bypass", res.Repo.Name) |
| 308 | + } |
| 309 | +} |
| 310 | + |
| 311 | +// saturateCreateLimiter pushes the per-actor counter for "repo_create" |
| 312 | +// up to the cap so the next non-admin Hit returns ErrThrottled. |
| 313 | +func saturateCreateLimiter(t *testing.T, deps repos.Deps, uid int64) { |
| 314 | + t.Helper() |
| 315 | + lim := throttle.Limit{ |
| 316 | + Scope: "repo_create", |
| 317 | + Identifier: fmt.Sprintf("user:%d", uid), |
| 318 | + Max: repos.CreateRateLimitMax, |
| 319 | + Window: repos.CreateRateLimitWindow, |
| 320 | + } |
| 321 | + for i := 0; i < repos.CreateRateLimitMax; i++ { |
| 322 | + if err := deps.Limiter.Hit(context.Background(), deps.Pool, lim); err != nil { |
| 323 | + t.Fatalf("priming limiter hit %d: %v", i, err) |
| 324 | + } |
| 325 | + } |
| 326 | +} |
| 327 | + |
| 268 | 328 | func TestCreate_RejectsBothOwnerKindsSet(t *testing.T) { |
| 269 | 329 | t.Parallel() |
| 270 | 330 | _, deps, uid, uname, _ := setupCreateEnv(t) |