| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | require "rails_helper" |
| 4 | require "tmpdir" |
| 5 | require "fileutils" |
| 6 | |
| 7 | # End-to-end checks for the rendered read experience: a README renders (not |
| 8 | # raw) on the repo landing page, source files get syntax highlighting + line |
| 9 | # numbers, .md blobs render, the permalink pins to a commit SHA, and malicious |
| 10 | # HTML in a README is neutralised. |
| 11 | RSpec.describe "Blob view", type: :request do |
| 12 | around do |example| |
| 13 | Dir.mktmpdir("blob-view-spec") do |tmp| |
| 14 | @repo_dir = File.join(tmp, "demo.git") |
| 15 | build_repo(@repo_dir, |
| 16 | "README.md" => "# Demo Project\n\nHello <script>alert('xss')</script> world\n\n- [x] done\n- [ ] todo\n", |
| 17 | "main.rb" => "def greet\n puts 'hi'\nend\n") |
| 18 | @sha = `git --git-dir #{@repo_dir} rev-parse main`.strip |
| 19 | example.run |
| 20 | end |
| 21 | end |
| 22 | |
| 23 | let(:user) { User.create!(smbcloud_id: 123_321, email: "blob-spec@example.com", username: "blobspec") } |
| 24 | let!(:repo) { user.repositories.create!(name: "demo", disk_path: @repo_dir, default_branch: "main") } |
| 25 | |
| 26 | it "renders the README as HTML on the repo landing page" do |
| 27 | get "/blobspec/demo" |
| 28 | expect(response).to have_http_status(:success) |
| 29 | expect(response.body).to include('<div class="px-6 py-6 prose-readme"') |
| 30 | expect(response.body).to match(%r{<h1[^>]*>.*Demo Project}m) |
| 31 | end |
| 32 | |
| 33 | it "neutralises malicious HTML in the README" do |
| 34 | get "/blobspec/demo" |
| 35 | # The payload is gone entirely; the only <script> tags left are the page |
| 36 | # layout's own (JSON-LD / importmap), never the README's. |
| 37 | expect(response.body).not_to include("alert('xss')") |
| 38 | rendered = response.body[/prose-readme.*?<\/div>/m] |
| 39 | expect(rendered).not_to include("<script") |
| 40 | end |
| 41 | |
| 42 | it "shows syntax highlighting and per-line anchors for a source file" do |
| 43 | get "/blobspec/demo/blob/main/main.rb" |
| 44 | expect(response).to have_http_status(:success) |
| 45 | expect(response.body).to include('id="L1"').and include('id="L2"') |
| 46 | expect(response.body).to include('class="highlight"') |
| 47 | end |
| 48 | |
| 49 | it "pins the copy-permalink base to the commit SHA, not the branch" do |
| 50 | get "/blobspec/demo/blob/main/main.rb" |
| 51 | expect(response.body).to include( |
| 52 | %(data-line-selection-permalink-base-value="/blobspec/demo/blob/#{@sha}/main.rb") |
| 53 | ) |
| 54 | end |
| 55 | |
| 56 | it "renders an .md blob as Markdown by default and as source with ?plain" do |
| 57 | get "/blobspec/demo/blob/main/README.md" |
| 58 | expect(response.body).to match(%r{<h1[^>]*>.*Demo Project}m) |
| 59 | |
| 60 | get "/blobspec/demo/blob/main/README.md?plain=1" |
| 61 | expect(response.body).to include('id="L1"') |
| 62 | end |
| 63 | |
| 64 | def build_repo(bare_path, files) |
| 65 | FileUtils.mkdir_p(bare_path) |
| 66 | system("git", "init", "--bare", bare_path, exception: true) |
| 67 | Dir.mktmpdir do |work| |
| 68 | system("git", "-C", work, "init", "-b", "main", exception: true) |
| 69 | system("git", "-C", work, "config", "user.email", "t@example.com", exception: true) |
| 70 | system("git", "-C", work, "config", "user.name", "Test", exception: true) |
| 71 | files.each { |name, content| File.write(File.join(work, name), content) } |
| 72 | system("git", "-C", work, "add", ".", exception: true) |
| 73 | system("git", "-C", work, "commit", "-m", "seed", exception: true) |
| 74 | system("git", "-C", work, "remote", "add", "origin", bare_path, exception: true) |
| 75 | system("git", "-C", work, "push", "origin", "main", exception: true) |
| 76 | end |
| 77 | end |
| 78 | end |