@@ -32,11 +32,13 @@ import ( |
| 32 | 32 | "github.com/tenseleyFlow/shithub/internal/auth/secretbox" |
| 33 | 33 | "github.com/tenseleyFlow/shithub/internal/infra/metrics" |
| 34 | 34 | "github.com/tenseleyFlow/shithub/internal/infra/storage" |
| 35 | + "github.com/tenseleyFlow/shithub/internal/ratelimit" |
| 35 | 36 | repogit "github.com/tenseleyFlow/shithub/internal/repos/git" |
| 36 | 37 | reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| 37 | 38 | "github.com/tenseleyFlow/shithub/internal/testing/dbtest" |
| 38 | 39 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
| 39 | 40 | apih "github.com/tenseleyFlow/shithub/internal/web/handlers/api" |
| 41 | + "github.com/tenseleyFlow/shithub/internal/web/handlers/api/apilimit" |
| 40 | 42 | workerdb "github.com/tenseleyFlow/shithub/internal/worker/sqlc" |
| 41 | 43 | ) |
| 42 | 44 | |
@@ -186,6 +188,53 @@ func TestRunnerHeartbeatClaimsQueuedJob(t *testing.T) { |
| 186 | 188 | } |
| 187 | 189 | } |
| 188 | 190 | |
| 191 | +func TestRunnerHeartbeatBypassesGlobalAnonAPILimit(t *testing.T) { |
| 192 | + pool := dbtest.NewTestDB(t) |
| 193 | + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) |
| 194 | + token, _ := registerRunnerForTest(t, pool, []string{"ubuntu-latest", "linux"}, 1) |
| 195 | + signer := runnerAPISigner(t, time.Date(2026, 5, 10, 12, 0, 0, 0, time.UTC)) |
| 196 | + |
| 197 | + h, err := apih.New(apih.Deps{ |
| 198 | + Pool: pool, |
| 199 | + Logger: logger, |
| 200 | + BaseURL: "https://shithub.test", |
| 201 | + RunnerJWT: signer, |
| 202 | + RateLimiter: ratelimit.New(pool), |
| 203 | + APILimit: apilimit.Config{ |
| 204 | + AuthedPerHour: 1, |
| 205 | + AnonPerHour: 1, |
| 206 | + Logger: logger, |
| 207 | + }, |
| 208 | + }) |
| 209 | + if err != nil { |
| 210 | + t.Fatalf("api.New: %v", err) |
| 211 | + } |
| 212 | + router := chi.NewRouter() |
| 213 | + h.Mount(router) |
| 214 | + |
| 215 | + req := httptest.NewRequest(http.MethodGet, "/api/v1/meta", nil) |
| 216 | + req.RemoteAddr = "10.0.0.77:12345" |
| 217 | + rr := httptest.NewRecorder() |
| 218 | + router.ServeHTTP(rr, req) |
| 219 | + if rr.Code != http.StatusOK { |
| 220 | + t.Fatalf("meta status: got %d, want 200; body=%s", rr.Code, rr.Body.String()) |
| 221 | + } |
| 222 | + |
| 223 | + req = httptest.NewRequest(http.MethodPost, "/api/v1/runners/heartbeat", |
| 224 | + strings.NewReader(`{"labels":["ubuntu-latest","linux"],"capacity":1}`)) |
| 225 | + req.Header.Set("Authorization", "Bearer "+token) |
| 226 | + req.RemoteAddr = "10.0.0.77:12346" |
| 227 | + rr = httptest.NewRecorder() |
| 228 | + router.ServeHTTP(rr, req) |
| 229 | + |
| 230 | + if rr.Code != http.StatusNoContent { |
| 231 | + t.Fatalf("heartbeat status: got %d, want 204; body=%s", rr.Code, rr.Body.String()) |
| 232 | + } |
| 233 | + if got := rr.Header().Get("X-RateLimit-Limit"); got != "60" { |
| 234 | + t.Errorf("runner heartbeat limit header: got %q, want 60", got) |
| 235 | + } |
| 236 | +} |
| 237 | + |
| 189 | 238 | func TestRunnerSecretsAreClaimedAndServerScrubsLogs(t *testing.T) { |
| 190 | 239 | ctx := context.Background() |
| 191 | 240 | pool := dbtest.NewTestDB(t) |