@@ -23,6 +23,7 @@ import ( |
| 23 | 23 | "github.com/tenseleyFlow/shithub/internal/auth/pat" |
| 24 | 24 | "github.com/tenseleyFlow/shithub/internal/auth/throttle" |
| 25 | 25 | "github.com/tenseleyFlow/shithub/internal/infra/storage" |
| 26 | + "github.com/tenseleyFlow/shithub/internal/orgs" |
| 26 | 27 | "github.com/tenseleyFlow/shithub/internal/repos" |
| 27 | 28 | "github.com/tenseleyFlow/shithub/internal/testing/dbtest" |
| 28 | 29 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
@@ -289,3 +290,53 @@ func TestGitHTTP_PushToArchivedRejected(t *testing.T) { |
| 289 | 290 | func writeFile(path, body string) error { |
| 290 | 291 | return os.WriteFile(path, []byte(body), 0o600) |
| 291 | 292 | } |
| 293 | + |
| 294 | +// TestGitHTTP_AnonCloneOrgOwnedPublic is a regression for an outage |
| 295 | +// on 2026-05-09: pushing to https://shithub.sh/tenseleyflow/shithub.git |
| 296 | +// returned 404 because authorizeForService only resolved owner-slugs |
| 297 | +// as users (the comment in handler.go admitted "orgs come in S31"). |
| 298 | +// Once that landed in production with an org-owned repo, the smart- |
| 299 | +// HTTP route became silently unusable for any org repo. |
| 300 | +func TestGitHTTP_AnonCloneOrgOwnedPublic(t *testing.T) { |
| 301 | + t.Parallel() |
| 302 | + env := setupEnv(t) |
| 303 | + |
| 304 | + // Create an org owned by alice and a public repo under it. |
| 305 | + org, err := orgs.Create(context.Background(), orgs.Deps{ |
| 306 | + Pool: env.pool, |
| 307 | + Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), |
| 308 | + Audit: audit.NewRecorder(), |
| 309 | + }, orgs.CreateParams{ |
| 310 | + Slug: "myorg", DisplayName: "MyOrg", CreatedByUserID: env.userID, |
| 311 | + }) |
| 312 | + if err != nil { |
| 313 | + t.Fatalf("orgs.Create: %v", err) |
| 314 | + } |
| 315 | + rfs, err := storage.NewRepoFS(env.root) |
| 316 | + if err != nil { |
| 317 | + t.Fatalf("NewRepoFS: %v", err) |
| 318 | + } |
| 319 | + rdeps := repos.Deps{ |
| 320 | + Pool: env.pool, RepoFS: rfs, Audit: audit.NewRecorder(), Limiter: throttle.NewLimiter(), |
| 321 | + Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), |
| 322 | + } |
| 323 | + if _, err := repos.Create(context.Background(), rdeps, repos.Params{ |
| 324 | + OwnerOrgID: org.ID, OwnerSlug: org.Slug, ActorUserID: env.userID, |
| 325 | + Name: "org-public", Visibility: "public", InitReadme: true, |
| 326 | + }); err != nil { |
| 327 | + t.Fatalf("create org repo: %v", err) |
| 328 | + } |
| 329 | + |
| 330 | + dst := filepath.Join(t.TempDir(), "clone") |
| 331 | + out, err := gitCmd("clone", env.srv.URL+"/myorg/org-public.git", dst).CombinedOutput() |
| 332 | + if err != nil { |
| 333 | + t.Fatalf("clone org-owned repo: %v\n%s", err, out) |
| 334 | + } |
| 335 | + out, err = gitCmd("-C", dst, "rev-list", "--count", "HEAD").CombinedOutput() |
| 336 | + if err != nil { |
| 337 | + t.Fatalf("rev-list: %v\n%s", err, out) |
| 338 | + } |
| 339 | + if got := strings.TrimSpace(string(out)); got != "1" { |
| 340 | + t.Fatalf("rev-list = %q, want 1", got) |
| 341 | + } |
| 342 | +} |