| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | require "rails_helper" |
| 4 | require "tmpdir" |
| 5 | require "fileutils" |
| 6 | require "base64" |
| 7 | |
| 8 | # The /raw endpoint is what install scripts, badges, and CI hit, so it must |
| 9 | # return the exact bytes with a sensible content-type and the nosniff guard |
| 10 | # that stops the browser from running user content as active content on the |
| 11 | # app origin. |
| 12 | RSpec.describe "Raw blob", type: :request do |
| 13 | around do |example| |
| 14 | Dir.mktmpdir("raw-blob-spec") do |tmp| |
| 15 | @repo_dir = File.join(tmp, "demo.git") |
| 16 | build_repo(@repo_dir, |
| 17 | "hello.txt" => "plain bytes\n", |
| 18 | "page.html" => "<h1>hi</h1>\n", |
| 19 | "logo.png" => png_bytes) |
| 20 | example.run |
| 21 | end |
| 22 | end |
| 23 | |
| 24 | let(:user) { User.create!(smbcloud_id: 987_654, email: "raw-spec@example.com", username: "rawspec") } |
| 25 | let!(:repo) { user.repositories.create!(name: "demo", disk_path: @repo_dir, default_branch: "main") } |
| 26 | |
| 27 | it "returns exact bytes with text/plain and nosniff for a text file" do |
| 28 | get "/rawspec/demo/raw/main/hello.txt" |
| 29 | expect(response).to have_http_status(:success) |
| 30 | expect(response.body).to eq("plain bytes\n") |
| 31 | expect(response.media_type).to match(%r{text/plain}) |
| 32 | expect(response.headers["X-Content-Type-Options"]).to eq("nosniff") |
| 33 | end |
| 34 | |
| 35 | it "serves HTML files as text/plain so they cannot run as active content" do |
| 36 | get "/rawspec/demo/raw/main/page.html" |
| 37 | expect(response).to have_http_status(:success) |
| 38 | expect(response.media_type).to match(%r{text/plain}) |
| 39 | expect(response.headers["X-Content-Type-Options"]).to eq("nosniff") |
| 40 | end |
| 41 | |
| 42 | it "serves images with their real content-type" do |
| 43 | get "/rawspec/demo/raw/main/logo.png" |
| 44 | expect(response).to have_http_status(:success) |
| 45 | expect(response.media_type).to eq("image/png") |
| 46 | expect(response.headers["X-Content-Type-Options"]).to eq("nosniff") |
| 47 | end |
| 48 | |
| 49 | it "returns 404 for a missing file" do |
| 50 | get "/rawspec/demo/raw/main/does-not-exist.txt" |
| 51 | expect(response).to have_http_status(:not_found) |
| 52 | end |
| 53 | |
| 54 | def build_repo(bare_path, files) |
| 55 | FileUtils.mkdir_p(bare_path) |
| 56 | system("git", "init", "--bare", bare_path, exception: true) |
| 57 | Dir.mktmpdir do |work| |
| 58 | system("git", "-C", work, "init", "-b", "main", exception: true) |
| 59 | system("git", "-C", work, "config", "user.email", "t@example.com", exception: true) |
| 60 | system("git", "-C", work, "config", "user.name", "Test", exception: true) |
| 61 | files.each { |name, content| File.binwrite(File.join(work, name), content) } |
| 62 | system("git", "-C", work, "add", ".", exception: true) |
| 63 | system("git", "-C", work, "commit", "-m", "seed", exception: true) |
| 64 | system("git", "-C", work, "remote", "add", "origin", bare_path, exception: true) |
| 65 | system("git", "-C", work, "push", "origin", "main", exception: true) |
| 66 | end |
| 67 | end |
| 68 | |
| 69 | # Smallest valid 1x1 PNG. |
| 70 | def png_bytes |
| 71 | Base64.decode64( |
| 72 | "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" |
| 73 | ) |
| 74 | end |
| 75 | end |