@@ -32,11 +32,13 @@ import ( |
| 32 | "github.com/tenseleyFlow/shithub/internal/auth/secretbox" | 32 | "github.com/tenseleyFlow/shithub/internal/auth/secretbox" |
| 33 | "github.com/tenseleyFlow/shithub/internal/infra/metrics" | 33 | "github.com/tenseleyFlow/shithub/internal/infra/metrics" |
| 34 | "github.com/tenseleyFlow/shithub/internal/infra/storage" | 34 | "github.com/tenseleyFlow/shithub/internal/infra/storage" |
| | 35 | + "github.com/tenseleyFlow/shithub/internal/ratelimit" |
| 35 | repogit "github.com/tenseleyFlow/shithub/internal/repos/git" | 36 | repogit "github.com/tenseleyFlow/shithub/internal/repos/git" |
| 36 | reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" | 37 | reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| 37 | "github.com/tenseleyFlow/shithub/internal/testing/dbtest" | 38 | "github.com/tenseleyFlow/shithub/internal/testing/dbtest" |
| 38 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" | 39 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
| 39 | apih "github.com/tenseleyFlow/shithub/internal/web/handlers/api" | 40 | apih "github.com/tenseleyFlow/shithub/internal/web/handlers/api" |
| | 41 | + "github.com/tenseleyFlow/shithub/internal/web/handlers/api/apilimit" |
| 40 | workerdb "github.com/tenseleyFlow/shithub/internal/worker/sqlc" | 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 | func TestRunnerSecretsAreClaimedAndServerScrubsLogs(t *testing.T) { | 238 | func TestRunnerSecretsAreClaimedAndServerScrubsLogs(t *testing.T) { |
| 190 | ctx := context.Background() | 239 | ctx := context.Background() |
| 191 | pool := dbtest.NewTestDB(t) | 240 | pool := dbtest.NewTestDB(t) |