test(seo): port social-unfurl tests to RSpec; add reverse-engineered spec

The MCP merge standardized the project on RSpec (rspec-rails, spec/). Port the Minitest suite added with the SEO work to RSpec request/model/service specs and remove test/, so there is one test framework. Specs use inline record creation to match the existing spec style. Also add .agents/specs/social-unfurls-and-seo.md: a reverse-engineered spec documenting the feature's routes, behavior, caching, privacy guards, acceptance criteria, and test map. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Seto Elkahfi committed Jun 30, 2026 at 19:33 UTC c4b6a07959eeede20d6b63fabbc3353db75ea362
14 files changed +468 -283
.agents/specs/social-unfurls-and-seo.md
+172
new file mode 100644 index 0000000..a7927e9 --- /dev/null +++ b/.agents/specs/social-unfurls-and-seo.md @@ -0,0 +1,172 @@ +# Spec: Social link unfurls and SEO for public repos + +Status: implemented (branch `feature/social-unfurls-seo`, merged to `development`). +Reverse-engineered from the shipped code so the behavior is documented and +testable. Scope is meta tags, Open Graph card images, and crawlability for +public repositories. + +## Problem + +A pasted siGit repo link produced no preview card in Slack, X, LinkedIn, and +Discord, and public repos were not cleanly indexable. Those are the channels +where developer tools spread, so a shared link should render a preview card and +leave a backlink. + +## Goals + +1. Every public repo page serves complete SEO and social meta tags in the + initial server HTML, because crawlers do not run JavaScript. +2. Each public repo has a generated 1200x630 preview image. +3. Public repos are crawlable and listed in the sitemap; private and + auth-gated pages are not, and leak nothing. + +## Non-goals + +Profile pages, stars/forks/explore/trending UI, full-text search, and +analytics. This work is meta tags, card images, and crawlability only. + +## Rendering model + +The app is server-rendered Rails (ERB + Hotwire), so meta tags and a crawlable +HTML body already appear in the initial response. No client-render workaround is +needed. + +## Routes + +| Method | Path | Controller | Purpose | +|--------|------|------------|---------| +| GET | `/og.png` | `og_images#default` | Generic sitewide card (default `og:image`). | +| GET | `/og/:username/:repository.png` | `og_images#show` | Per-repo card. Dotted names allowed (`Qwen2.5-GGUF`), `format: false`. | +| GET | `/sitemap.xml` | `sitemaps#index` | Public repos plus their owners (pre-existing). | + +Both `/og` routes are declared before the `/:username` matcher so they are not +read as profiles. + +## Behavior + +### Meta tags (all pages) + +Driven by `SeoHelper` and the `shared/_seo` partial. Defaults degrade so any +page emits valid, non-empty tags. Pages override with `content_for`: +`:title`, `:description`, `:og_image`, `:og_image_alt`, `:og_type`, `:robots`, +`:structured_data`. + +Emitted: `<title>`, `meta description`, `link canonical` (path only, no query or +fragment), Open Graph (`og:type`, `og:site_name` = `siGit`, `og:title`, +`og:description`, `og:url`, `og:image`, `og:image:type`, `og:image:width` = +1200, `og:image:height` = 630, `og:image:alt`, `og:locale`), Twitter +(`summary_large_image`, title, description, image, image:alt), and JSON-LD +(`Organization` + `WebSite` sitewide). + +### Public repo pages + +`repositories#show`, `#model`, `#tree`, `#cicd`, and `blobs#show` render +`shared/_repository_social`, which sets the description, the per-repo +`og:image` (`/og/:owner/:repo.png`), and the image alt text. The two canonical +repo pages (`show`, `model`) also emit `SoftwareSourceCode` JSON-LD with name, +description, repo URL, author, dates, image, and `programmingLanguage` when a +primary language is detected. + +`Repository#seo_description` returns the description when present, otherwise a +fallback that names the owner and the kind (code repo or open-weights model). +This guarantees non-empty social text. + +### OG card images + +`OgImageService` builds an SVG from the brand template and rasterizes it to PNG +with libvips. Card content: repo name, `@owner`, wrapped description (up to +three lines), primary-language dot with color, star count when above zero, and +the siGit wordmark. The generic card carries the tagline only. + +All user-controlled text is XML-escaped before going into the SVG. + +Primary language is guessed from file extensions on the default branch +(`Repository#primary_language`), using a small Linguist-style color map. Model +repos that carry weight files surface the weight format instead of a code +language. + +### Caching and performance + +`OgImagesController` answers conditional GETs with `stale?(etag:, public:)`. The +ETag is `OgImageService::TEMPLATE_VERSION` plus `Repository#og_version`, a hash +of repo id, name, description, star count, default-branch tip commit, and +`updated_at`. A repeat fetch with a matching `If-None-Match` returns 304. Bytes +are cached in `Rails.cache` keyed by the same version, so the SVG is rasterized +only on a cache miss, never on the hot path when a card exists. Responses set +`Cache-Control: public` with a long max-age. + +The template version bumps invalidate every cached card. The content hash +invalidates a single repo's card when its content changes. + +### Fallback + +If rasterization is unavailable (no libvips, no SVG or font support), the +endpoint logs a warning and serves the static brand icon +(`public/favicon/web-app-manifest-512x512.png`) with a short TTL, so a transient +failure self-heals and the endpoint never returns 500. + +### Privacy guards + +- A private repo returns 404 on `/og/:owner/:repo.png` for everyone, including + the owner, so no card can leak. +- Visibility is decided server-side from the repo's `is_private` flag, not a + client signal. A logged-out request to a private repo page renders the static + 404 with no metadata. +- Private repos and users who own only private repos are excluded from + `sitemap.xml`. +- Auth and transactional pages (`sessions`, `registrations`, `passwords`, + `confirmations`, `billing`, `users#settings`, `repositories#new`) call + `noindex!` in the controller, which makes `SeoHelper#meta_robots` emit + `noindex, nofollow`. + +### robots.txt + +`public/robots.txt` allows content, disallows `/auth`, `/settings`, `/billing`, +`/new`, `/api/`, and `/*/raw/`, and points at `/sitemap.xml`. + +## Acceptance criteria + +1. A public repo URL pasted into Slack, X, LinkedIn, or Discord shows a + `summary_large_image` card with the correct title, description, and image. +2. `view-source` on a public repo page shows the OG, Twitter, and canonical + tags in the initial HTML. +3. `/og/:owner/:repo.png` returns a 1200x630 PNG, is a 304 on the second hit + with a matching ETag, and has a fallback for missing metadata. +4. A private repo URL returns no preview, carries `noindex`, and is absent from + `sitemap.xml`. +5. `robots.txt` and `sitemap.xml` validate; public repos appear in the sitemap, + auth and non-content routes do not. + +## Production dependency + +The runtime image installs `libvips`, `librsvg2-2`, and `fonts-dejavu-core` so +libvips can rasterize SVG text. macOS dev hosts usually lack libvips, so the +endpoint returns the static fallback locally; the card renders in CI and +production. + +## Tests + +RSpec, the project framework. + +- `spec/requests/repository_seo_spec.rb`: meta output for public vs private, + JSON-LD, indexable flag, empty-description fallback, noindex on auth pages. +- `spec/requests/og_images_spec.rb`: PNG content type, cache headers, 304 on + conditional GET, dotted names, private 404, unknown 404, default card. +- `spec/requests/sitemap_robots_spec.rb`: sitemap inclusion and exclusion, + robots directives. +- `spec/models/repository_seo_spec.rb`: `seo_description`, `og_version`, + `primary_language`. +- `spec/services/og_image_service_spec.rb`: SVG structure, XML-escaping, default + card, render-or-typed-error. + +## Key files + +- `app/services/og_image_service.rb`, `app/controllers/og_images_controller.rb` +- `app/helpers/seo_helper.rb`, `app/views/shared/_seo.html.erb`, + `app/views/shared/_repository_social.html.erb` +- `app/models/repository.rb` (`seo_description`, `primary_language`, + `og_version`), `app/services/git_repository_service.rb` (`head_sha`, + `tree_filenames`) +- `app/controllers/application_controller.rb` (`noindex!`), `config/routes.rb`, + `public/robots.txt`, `Dockerfile` +- `docs/seo-and-social-unfurls.md` (how to verify)
spec/models/repository_seo_spec.rb
+47
new file mode 100644 index 0000000..6407d91 --- /dev/null +++ b/spec/models/repository_seo_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Repository, type: :model do + let(:user) { User.create!(smbcloud_id: 8001, email: "alice@example.com", username: "alice") } + + def build_repo(**attrs) + user.repositories.create!( + { name: "widget", kind: "code", default_branch: "main", + disk_path: "/nonexistent/alice/widget.git" }.merge(attrs) + ) + end + + describe "#seo_description" do + it "uses the description when present" do + repo = build_repo(description: "A tiny widget library.") + expect(repo.seo_description).to eq("A tiny widget library.") + end + + it "falls back for a code repo without a description" do + repo = build_repo(description: nil) + expect(repo.seo_description).to include("@alice") + expect(repo.seo_description).to include("Git hosting built for AI workflows") + end + + it "falls back for a model repo without a description" do + repo = build_repo(name: "Qwen2.5-GGUF", kind: "model", description: nil) + expect(repo.seo_description).to include("open-weights model") + end + end + + describe "#og_version" do + it "changes when a share-card input changes" do + repo = build_repo(stars_count: 1) + before = repo.og_version + repo.stars_count = 2 + expect(repo.og_version).not_to eq(before) + end + end + + describe "#primary_language" do + it "is nil for an uninitialized repo" do + expect(build_repo.primary_language).to be_nil + end + end +end
spec/requests/og_images_spec.rb
+74
new file mode 100644 index 0000000..59b4a64 --- /dev/null +++ b/spec/requests/og_images_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "rails_helper" + +# The OG endpoint must always answer crawlers with a cacheable PNG (a generated +# card where libvips is available, a static fallback otherwise) and must never +# render a card for a private repo. +RSpec.describe "OG share-card images", type: :request do + let(:owner) { User.create!(smbcloud_id: 6001, email: "alice@example.com", username: "alice") } + let(:other) { User.create!(smbcloud_id: 6002, email: "bob@example.com", username: "bob") } + + let!(:public_repo) do + owner.repositories.create!( + name: "widget", description: "A tiny widget library.", + kind: "code", default_branch: "main", + disk_path: "/nonexistent/alice/widget.git", is_private: false, stars_count: 7 + ) + end + + let!(:dotted_repo) do + owner.repositories.create!( + name: "Qwen2.5-GGUF", description: "Quantized weights.", + kind: "model", default_branch: "main", + disk_path: "/nonexistent/alice/qwen.git", is_private: false + ) + end + + let!(:private_repo) do + other.repositories.create!( + name: "secret-internal-tool", description: "private", + kind: "code", default_branch: "main", + disk_path: "/nonexistent/bob/secret.git", is_private: true + ) + end + + it "returns a cacheable PNG for a public repo" do + get "/og/alice/widget.png" + expect(response).to have_http_status(:ok) + expect(response.media_type).to eq("image/png") + expect(response.headers["Cache-Control"]).to include("public") + expect(response.headers["ETag"]).to be_present + end + + it "routes dotted repo names correctly" do + get "/og/alice/Qwen2.5-GGUF.png" + expect(response).to have_http_status(:ok) + expect(response.media_type).to eq("image/png") + end + + it "serves a 304 on a conditional GET with a matching ETag" do + get "/og/alice/widget.png" + etag = response.headers["ETag"] + expect(etag).to be_present + + get "/og/alice/widget.png", headers: { "If-None-Match" => etag } + expect(response).to have_http_status(:not_modified) + end + + it "renders the generic sitewide card" do + get "/og.png" + expect(response).to have_http_status(:ok) + expect(response.media_type).to eq("image/png") + end + + it "404s for a private repo (no card, even though it exists)" do + get "/og/bob/secret-internal-tool.png" + expect(response).to have_http_status(:not_found) + end + + it "404s for an unknown repo" do + get "/og/alice/does-not-exist.png" + expect(response).to have_http_status(:not_found) + end +end
spec/requests/repository_seo_spec.rb
+79
new file mode 100644 index 0000000..7b3b1a0 --- /dev/null +++ b/spec/requests/repository_seo_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "rails_helper" + +# The SEO/social meta tags must be in the initial server HTML — crawlers don't +# run JS — and must never expose anything about a private repo. +RSpec.describe "Repository SEO meta tags", type: :request do + let(:owner) { User.create!(smbcloud_id: 5001, email: "alice@example.com", username: "alice") } + let(:other) { User.create!(smbcloud_id: 5002, email: "bob@example.com", username: "bob") } + + let!(:public_repo) do + owner.repositories.create!( + name: "widget", description: "A tiny widget library for building UIs fast.", + kind: "code", default_branch: "main", + disk_path: "/nonexistent/alice/widget.git", is_private: false, stars_count: 7 + ) + end + + let!(:private_repo) do + other.repositories.create!( + name: "secret-internal-tool", description: "TOPSECRETDESCRIPTION should never leak to crawlers.", + kind: "code", default_branch: "main", + disk_path: "/nonexistent/bob/secret.git", is_private: true + ) + end + + describe "a public repo page" do + before { get "/alice/widget" } + + it "responds 200" do + expect(response).to have_http_status(:ok) + end + + it "emits Open Graph, Twitter, and canonical tags" do + expect(response.body).to include('property="og:title" content="alice/widget"') + expect(response.body).to include('property="og:site_name" content="siGit"') + expect(response.body).to include('property="og:image" content="http://www.example.com/og/alice/widget.png"') + expect(response.body).to include('property="og:image:width" content="1200"') + expect(response.body).to include('property="og:image:height" content="630"') + expect(response.body).to include('name="twitter:card" content="summary_large_image"') + expect(response.body).to include('rel="canonical" href="http://www.example.com/alice/widget"') + expect(response.body).to include("A tiny widget library") + end + + it "emits SoftwareSourceCode JSON-LD" do + expect(response.body).to include('"@type":"SoftwareSourceCode"') + expect(response.body).to include('"codeRepository":"http://www.example.com/alice/widget"') + end + + it "is indexable" do + expect(response.body).to include('name="robots" content="index, follow"') + end + end + + it "still emits a non-empty description when the repo has none" do + public_repo.update_column(:description, nil) + get "/alice/widget" + expect(response).to have_http_status(:ok) + expect(response.body).to include("Git hosting built for AI workflows") + end + + describe "a private repo" do + it "404s and leaks no metadata to a logged-out crawler" do + get "/bob/secret-internal-tool" + expect(response).to have_http_status(:not_found) + expect(response.body).not_to include("TOPSECRETDESCRIPTION") + expect(response.body).not_to include("secret-internal-tool") + expect(response.body).not_to include("og/bob") + end + end + + describe "auth pages" do + it "are marked noindex" do + get "/auth" + expect(response).to have_http_status(:ok) + expect(response.body).to include('name="robots" content="noindex, nofollow"') + end + end +end
spec/requests/sitemap_robots_spec.rb
+50
new file mode 100644 index 0000000..6d2e98a --- /dev/null +++ b/spec/requests/sitemap_robots_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rails_helper" + +# robots.txt and sitemap.xml must expose public content and exclude anything +# private or behind auth. +RSpec.describe "Sitemap and robots", type: :request do + let(:alice) { User.create!(smbcloud_id: 7001, email: "alice@example.com", username: "alice") } + let(:bob) { User.create!(smbcloud_id: 7002, email: "bob@example.com", username: "bob") } + + before do + alice.repositories.create!(name: "widget", kind: "code", default_branch: "main", + disk_path: "/nonexistent/alice/widget.git", is_private: false) + alice.repositories.create!(name: "Qwen2.5-GGUF", kind: "model", default_branch: "main", + disk_path: "/nonexistent/alice/qwen.git", is_private: false) + bob.repositories.create!(name: "secret-internal-tool", kind: "code", default_branch: "main", + disk_path: "/nonexistent/bob/secret.git", is_private: true) + end + + describe "sitemap.xml" do + before { get "/sitemap.xml" } + + it "lists public repos and their owners" do + expect(response).to have_http_status(:ok) + expect(response.media_type).to eq("application/xml") + expect(response.body).to include("http://www.example.com/alice/widget") + expect(response.body).to include("http://www.example.com/alice/Qwen2.5-GGUF") + expect(response.body).to include("http://www.example.com/alice") + end + + it "excludes private repos and private-only users" do + expect(response.body).not_to include("secret-internal-tool") + expect(response.body).not_to include("http://www.example.com/bob") + end + end + + describe "robots.txt" do + it "allows content and disallows auth/transactional routes" do + get "/robots.txt" + expect(response).to have_http_status(:ok) + body = response.body + expect(body).to include("Sitemap: https://sigit.si/sitemap.xml") + expect(body).to include("Disallow: /settings") + expect(body).to include("Disallow: /billing") + expect(body).to include("Disallow: /auth") + expect(body).to include("Disallow: /new") + expect(body).to include("Disallow: /api/") + end + end +end
spec/services/og_image_service_spec.rb
+46
new file mode 100644 index 0000000..51b4b6c --- /dev/null +++ b/spec/services/og_image_service_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe OgImageService do + let(:user) { User.create!(smbcloud_id: 9001, email: "alice@example.com", username: "alice") } + let(:repo) do + user.repositories.create!( + name: "widget", description: "A tiny widget library.", + kind: "code", default_branch: "main", + disk_path: "/nonexistent/alice/widget.git", stars_count: 7 + ) + end + + it "builds a well-formed SVG with the repo name and owner" do + svg = described_class.send(:repository_svg, repo) + expect(svg).to start_with("<svg") + expect(svg).to include("@alice /") + expect(svg).to include("widget") + expect(svg).to include('width="1200"') + expect(svg).to include('height="630"') + end + + it "XML-escapes user-controlled text to prevent SVG injection" do + repo.name = %q{x"><script>alert(1)</script>} + repo.description = "evil & <b>markup</b>" + svg = described_class.send(:repository_svg, repo) + expect(svg).not_to include("<script>") + expect(svg).to include("&lt;script&gt;") + expect(svg).to include("&amp;") + end + + it "builds the default card without a repository" do + svg = described_class.send(:default_svg) + expect(svg).to start_with("<svg") + expect(svg).to include("Git hosting for the AI era") + end + + it "renders PNG bytes, or raises a typed (catchable) error without libvips" do + png = described_class.render_default + expect(png).to start_with("\x89PNG".b) + rescue OgImageService::Error + # Graceful, controller-catchable failure on hosts without libvips/SVG support. + expect(true).to be(true) + end +end
test/controllers/og_images_controller_test.rb
-54
deleted file mode 100644 index 159b7af..0000000 --- a/test/controllers/og_images_controller_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -# The OG endpoint must always answer crawlers with a PNG (a generated card where -# libvips is available, a static fallback otherwise), cache aggressively, and -# never render a card for a private repo. -class OgImagesControllerTest < ActionDispatch::IntegrationTest - setup do - @public = repositories(:public_code) - @model = repositories(:public_model) - @private = repositories(:private_code) - end - - test "public repo OG image returns a cacheable PNG" do - get "/og/#{@public.user.username}/#{@public.name}.png" - assert_response :success - assert_equal "image/png", response.media_type - assert_includes response.headers["Cache-Control"], "public" - assert response.headers["ETag"].present?, "expected an ETag for conditional GETs" - end - - test "dotted repo names route correctly" do - get "/og/#{@model.user.username}/#{@model.name}.png" - assert_response :success - assert_equal "image/png", response.media_type - end - - test "second hit with matching ETag is served from cache as 304" do - get "/og/#{@public.user.username}/#{@public.name}.png" - etag = response.headers["ETag"] - assert etag.present? - - get "/og/#{@public.user.username}/#{@public.name}.png", - headers: { "If-None-Match" => etag } - assert_response :not_modified - end - - test "default sitewide card returns a PNG" do - get "/og.png" - assert_response :success - assert_equal "image/png", response.media_type - end - - test "private repo OG image is not rendered (404)" do - get "/og/#{@private.user.username}/#{@private.name}.png" - assert_response :not_found - end - - test "unknown repo OG image is 404" do - get "/og/alice/does-not-exist.png" - assert_response :not_found - end -end
test/fixtures/repositories.yml
-29
deleted file mode 100644 index d818cd4..0000000 --- a/test/fixtures/repositories.yml +++ /dev/null @@ -1,29 +0,0 @@ -public_code: - user: alice - name: widget - description: A tiny widget library for building UIs fast. - default_branch: main - kind: code - disk_path: /nonexistent/alice/widget.git - is_private: false - stars_count: 7 - -public_model: - user: alice - name: Qwen2.5-GGUF - description: Quantized GGUF weights for fast local inference. - default_branch: main - kind: model - disk_path: /nonexistent/alice/qwen.git - is_private: false - stars_count: 0 - -private_code: - user: bob - name: secret-internal-tool - description: TOPSECRETDESCRIPTION should never leak to crawlers. - default_branch: main - kind: code - disk_path: /nonexistent/bob/secret.git - is_private: true - stars_count: 3
test/fixtures/users.yml
-11
deleted file mode 100644 index 1863e79..0000000 --- a/test/fixtures/users.yml +++ /dev/null @@ -1,11 +0,0 @@ -alice: - username: alice - email: alice@example.com - smbcloud_id: 1001 - display_name: Alice - -bob: - username: bob - email: bob@example.com - smbcloud_id: 1002 - display_name: Bob
test/integration/repository_seo_test.rb
-60
deleted file mode 100644 index cdabc1d..0000000 --- a/test/integration/repository_seo_test.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -# Verifies the server-rendered SEO/social meta tags for public vs private repos. -# Crawlers don't run JS, so everything asserted here must be in the initial HTML. -class RepositorySeoTest < ActionDispatch::IntegrationTest - setup do - @public = repositories(:public_code) - @private = repositories(:private_code) - end - - test "public repo page emits Open Graph, Twitter, and canonical tags server-side" do - get "/#{@public.user.username}/#{@public.name}" - assert_response :success - - assert_includes response.body, %(property="og:title" content="alice/widget") - assert_includes response.body, %(property="og:site_name" content="siGit") - assert_includes response.body, %(property="og:image" content="http://www.example.com/og/alice/widget.png") - assert_includes response.body, %(property="og:image:width" content="1200") - assert_includes response.body, %(property="og:image:height" content="630") - assert_includes response.body, %(name="twitter:card" content="summary_large_image") - assert_includes response.body, %(rel="canonical" href="http://www.example.com/alice/widget") - assert_includes response.body, "A tiny widget library" - end - - test "public repo page emits SoftwareSourceCode JSON-LD" do - get "/#{@public.user.username}/#{@public.name}" - assert_response :success - assert_includes response.body, %("@type":"SoftwareSourceCode") - assert_includes response.body, %("codeRepository":"http://www.example.com/alice/widget") - end - - test "public repo page is indexable" do - get "/#{@public.user.username}/#{@public.name}" - assert_includes response.body, %(name="robots" content="index, follow") - end - - test "private repo returns 404 and leaks no metadata to a logged-out crawler" do - get "/#{@private.user.username}/#{@private.name}" - assert_response :not_found - refute_includes response.body, "TOPSECRETDESCRIPTION" - refute_includes response.body, "secret-internal-tool" - refute_includes response.body, "og/bob" - end - - test "auth pages are marked noindex" do - get "/auth" - assert_response :success - assert_includes response.body, %(name="robots" content="noindex, nofollow") - end - - test "empty-description repo still gets a non-empty description tag" do - @public.update_column(:description, nil) - get "/#{@public.user.username}/#{@public.name}" - assert_response :success - # Falls back to the generated seo_description rather than an empty tag. - assert_includes response.body, "Git hosting built for AI workflows" - end -end
test/integration/sitemap_robots_test.rb
-35
deleted file mode 100644 index df4fd2e..0000000 --- a/test/integration/sitemap_robots_test.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -# robots.txt and sitemap.xml must expose public content and exclude anything -# private or behind auth. -class SitemapRobotsTest < ActionDispatch::IntegrationTest - test "sitemap lists public repos and their owners" do - get "/sitemap.xml" - assert_response :success - assert_equal "application/xml", response.media_type - - assert_includes response.body, "http://www.example.com/alice/widget" - assert_includes response.body, "http://www.example.com/alice/Qwen2.5-GGUF" - assert_includes response.body, "http://www.example.com/alice" - end - - test "sitemap excludes private repos and users who only have private repos" do - get "/sitemap.xml" - refute_includes response.body, "secret-internal-tool" - refute_includes response.body, "http://www.example.com/bob" - end - - test "robots.txt allows content and disallows auth/transactional routes" do - get "/robots.txt" - assert_response :success - - body = response.body - assert_includes body, "Sitemap: https://sigit.si/sitemap.xml" - assert_includes body, "Disallow: /settings" - assert_includes body, "Disallow: /billing" - assert_includes body, "Disallow: /auth" - assert_includes body, "Disallow: /api/" - end -end
test/models/repository_seo_test.rb
-35
deleted file mode 100644 index 4eff185..0000000 --- a/test/models/repository_seo_test.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class RepositoryModelSeoTest < ActiveSupport::TestCase - test "seo_description uses the description when present" do - repo = repositories(:public_code) - assert_equal "A tiny widget library for building UIs fast.", repo.seo_description - end - - test "seo_description falls back for a code repo without a description" do - repo = repositories(:public_code) - repo.description = nil - assert_includes repo.seo_description, "@alice" - assert_includes repo.seo_description, "Git hosting built for AI workflows" - end - - test "seo_description falls back for a model repo without a description" do - repo = repositories(:public_model) - repo.description = nil - assert_includes repo.seo_description, "open-weights model" - end - - test "og_version changes when share-card inputs change" do - repo = repositories(:public_code) - before = repo.og_version - repo.stars_count = repo.stars_count + 1 - refute_equal before, repo.og_version - end - - test "primary_language is nil for an uninitialized repo" do - # Fixture disk_path does not exist on disk. - assert_nil repositories(:public_code).primary_language - end -end
test/services/og_image_service_test.rb
-44
deleted file mode 100644 index 93a50fb..0000000 --- a/test/services/og_image_service_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class OgImageServiceTest < ActiveSupport::TestCase - test "repository SVG includes the repo name, owner, and is well-formed" do - repo = repositories(:public_code) - svg = OgImageService.send(:repository_svg, repo) - - assert svg.start_with?("<svg") - assert_includes svg, "@alice /" - assert_includes svg, "widget" - assert_includes svg, %(width="1200") - assert_includes svg, %(height="630") - end - - test "user-controlled text is XML-escaped to prevent SVG injection" do - repo = repositories(:public_code) - repo.name = %q{x"><script>alert(1)</script>} - repo.description = "evil & <b>markup</b>" - svg = OgImageService.send(:repository_svg, repo) - - refute_includes svg, "<script>" - assert_includes svg, "&lt;script&gt;" - assert_includes svg, "&amp;" - end - - test "default card is generated without a repository" do - svg = OgImageService.send(:default_svg) - assert svg.start_with?("<svg") - assert_includes svg, "Git hosting for the AI era" - end - - test "render raises a typed error when libvips/SVG support is unavailable" do - # In CI/local without libvips this exercises the fallback path the - # controller relies on; where libvips exists it returns PNG bytes instead. - begin - png = OgImageService.render_default - assert png.start_with?("\x89PNG".b), "expected PNG magic bytes" - rescue OgImageService::Error - assert true # graceful, controller-catchable failure - end - end -end
test/test_helper.rb
-15
deleted file mode 100644 index 9614c9f..0000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -ENV["RAILS_ENV"] ||= "test" -require_relative "../config/environment" -require "rails/test_help" - -module ActiveSupport - class TestCase - # Run tests in parallel with specified workers - parallelize(workers: :number_of_processors) - - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - fixtures :all - end -end