| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | require "rails_helper" |
| 4 | |
| 5 | # The SEO/social meta tags must be in the initial server HTML — crawlers don't |
| 6 | # run JS — and must never expose anything about a private repo. |
| 7 | RSpec.describe "Repository SEO meta tags", type: :request do |
| 8 | let(:owner) { User.create!(smbcloud_id: 5001, email: "alice@example.com", username: "alice") } |
| 9 | let(:other) { User.create!(smbcloud_id: 5002, email: "bob@example.com", username: "bob") } |
| 10 | |
| 11 | let!(:public_repo) do |
| 12 | owner.repositories.create!( |
| 13 | name: "widget", description: "A tiny widget library for building UIs fast.", |
| 14 | kind: "code", default_branch: "main", |
| 15 | disk_path: "/nonexistent/alice/widget.git", is_private: false, stars_count: 7 |
| 16 | ) |
| 17 | end |
| 18 | |
| 19 | let!(:private_repo) do |
| 20 | other.repositories.create!( |
| 21 | name: "secret-internal-tool", description: "TOPSECRETDESCRIPTION should never leak to crawlers.", |
| 22 | kind: "code", default_branch: "main", |
| 23 | disk_path: "/nonexistent/bob/secret.git", is_private: true |
| 24 | ) |
| 25 | end |
| 26 | |
| 27 | describe "a public repo page" do |
| 28 | before { get "/alice/widget" } |
| 29 | |
| 30 | it "responds 200" do |
| 31 | expect(response).to have_http_status(:ok) |
| 32 | end |
| 33 | |
| 34 | it "emits Open Graph, Twitter, and canonical tags" do |
| 35 | expect(response.body).to include('property="og:title" content="alice/widget"') |
| 36 | expect(response.body).to include('property="og:site_name" content="siGit"') |
| 37 | expect(response.body).to include('property="og:image" content="http://www.example.com/og/alice/widget.png"') |
| 38 | expect(response.body).to include('property="og:image:width" content="1200"') |
| 39 | expect(response.body).to include('property="og:image:height" content="630"') |
| 40 | expect(response.body).to include('name="twitter:card" content="summary_large_image"') |
| 41 | expect(response.body).to include('rel="canonical" href="http://www.example.com/alice/widget"') |
| 42 | expect(response.body).to include("A tiny widget library") |
| 43 | end |
| 44 | |
| 45 | it "emits SoftwareSourceCode JSON-LD" do |
| 46 | expect(response.body).to include('"@type":"SoftwareSourceCode"') |
| 47 | expect(response.body).to include('"codeRepository":"http://www.example.com/alice/widget"') |
| 48 | end |
| 49 | |
| 50 | it "is indexable" do |
| 51 | expect(response.body).to include('name="robots" content="index, follow"') |
| 52 | end |
| 53 | end |
| 54 | |
| 55 | it "still emits a non-empty description when the repo has none" do |
| 56 | public_repo.update_column(:description, nil) |
| 57 | get "/alice/widget" |
| 58 | expect(response).to have_http_status(:ok) |
| 59 | expect(response.body).to include("Git hosting built for AI workflows") |
| 60 | end |
| 61 | |
| 62 | describe "a private repo" do |
| 63 | it "404s and leaks no metadata to a logged-out crawler" do |
| 64 | get "/bob/secret-internal-tool" |
| 65 | expect(response).to have_http_status(:not_found) |
| 66 | expect(response.body).not_to include("TOPSECRETDESCRIPTION") |
| 67 | expect(response.body).not_to include("secret-internal-tool") |
| 68 | expect(response.body).not_to include("og/bob") |
| 69 | end |
| 70 | end |
| 71 | |
| 72 | describe "auth pages" do |
| 73 | it "are marked noindex" do |
| 74 | get "/auth" |
| 75 | expect(response).to have_http_status(:ok) |
| 76 | expect(response.body).to include('name="robots" content="noindex, nofollow"') |
| 77 | end |
| 78 | end |
| 79 | end |