| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | require "rails_helper" |
| 4 | require "tmpdir" |
| 5 | require "fileutils" |
| 6 | |
| 7 | RSpec.describe RepositoryImportJob, type: :job do |
| 8 | let(:user) { User.create!(smbcloud_id: 7, email: "imp@example.com", username: "importer") } |
| 9 | |
| 10 | around do |example| |
| 11 | Dir.mktmpdir("import-job-spec") do |tmp| |
| 12 | @tmp = tmp |
| 13 | @repos = File.join(tmp, "repos") |
| 14 | @source = build_source(File.join(tmp, "src")) |
| 15 | example.run |
| 16 | end |
| 17 | end |
| 18 | |
| 19 | before do |
| 20 | # Point repo storage at the sandbox for both the service and the job. |
| 21 | allow(GitRepositoryService).to receive(:repo_path) do |u, r| |
| 22 | File.join(@repos, u, "#{r}.git") |
| 23 | end |
| 24 | end |
| 25 | |
| 26 | def build_source(dir) |
| 27 | work = File.join(dir, "work") |
| 28 | FileUtils.mkdir_p(work) |
| 29 | system("git", "init", "-q", "-b", "main", work, exception: true) |
| 30 | system("git", "-C", work, "config", "user.email", "t@e.st", exception: true) |
| 31 | system("git", "-C", work, "config", "user.name", "T", exception: true) |
| 32 | File.write(File.join(work, "README.md"), "# Imported\n\nHello.\n") |
| 33 | system("git", "-C", work, "add", ".", exception: true) |
| 34 | system("git", "-C", work, "commit", "-qm", "init", exception: true) |
| 35 | system("git", "-C", work, "tag", "v1", exception: true) |
| 36 | system("git", "-C", work, "checkout", "-q", "-b", "dev", exception: true) |
| 37 | File.write(File.join(work, "d.txt"), "d\n") |
| 38 | system("git", "-C", work, "add", ".", exception: true) |
| 39 | system("git", "-C", work, "commit", "-qm", "dev", exception: true) |
| 40 | system("git", "-C", work, "checkout", "-q", "main", exception: true) |
| 41 | bare = File.join(dir, "source.git") |
| 42 | system("git", "clone", "-q", "--bare", work, bare, exception: true) |
| 43 | bare |
| 44 | end |
| 45 | |
| 46 | def new_import(attrs = {}) |
| 47 | user.repository_imports.create!({ |
| 48 | source_url: @source, source_host: nil, mode: "migrate", |
| 49 | target_name: "imported", target_private: false, default_branch: "main", |
| 50 | status: "queued" |
| 51 | }.merge(attrs)) |
| 52 | end |
| 53 | |
| 54 | it "migrates a repo with all branches, tags, history and a working default branch" do |
| 55 | import = new_import |
| 56 | |
| 57 | described_class.perform_now(import.id) |
| 58 | import.reload |
| 59 | |
| 60 | expect(import.status).to eq("done") |
| 61 | repo = import.repository |
| 62 | expect(repo).to be_present |
| 63 | expect(repo).to be_initialized |
| 64 | |
| 65 | branches = GitRepositoryService.branches(repo.disk_path).sort |
| 66 | expect(branches).to eq(%w[dev main]) |
| 67 | expect(`git --git-dir #{repo.disk_path} tag`.split).to eq(%w[v1]) |
| 68 | expect(GitRepositoryService.default_branch(repo.disk_path)).to eq("main") |
| 69 | # README is reachable on the default branch → landing page will render it. |
| 70 | expect(GitRepositoryService.readme_content(repo.disk_path, "main")).to be_present |
| 71 | end |
| 72 | |
| 73 | it "sets up a read-only mirror in mirror mode" do |
| 74 | import = new_import(mode: "mirror") |
| 75 | |
| 76 | described_class.perform_now(import.id) |
| 77 | import.reload |
| 78 | repo = import.repository |
| 79 | |
| 80 | expect(import.status).to eq("done") |
| 81 | expect(repo.mirror?).to be(true) |
| 82 | expect(repo.upstream_url).to eq(@source) |
| 83 | expect(repo.mirror_status).to eq("ok") |
| 84 | expect(repo.mirror_synced_at).to be_present |
| 85 | expect(repo.writable_by?(user)).to be(false) |
| 86 | end |
| 87 | |
| 88 | it "leaves no half-created repo behind when the import fails, and stays retryable" do |
| 89 | import = new_import |
| 90 | allow(RepositoryImportService).to receive(:clone_and_push) |
| 91 | .and_raise(RepositoryImportService::ImportError, "boom") |
| 92 | |
| 93 | described_class.perform_now(import.id) |
| 94 | import.reload |
| 95 | |
| 96 | expect(import.status).to eq("failed") |
| 97 | expect(import.error_message).to eq("boom") |
| 98 | expect(import.repository).to be_nil |
| 99 | expect(user.repositories.count).to eq(0) |
| 100 | expect(Dir.exist?(File.join(@repos, user.username, "imported.git"))).to be(false) |
| 101 | expect(import.retryable?).to be(true) |
| 102 | end |
| 103 | |
| 104 | it "rejects a name collision with a different existing repo, without touching it" do |
| 105 | existing = user.repositories.create!( |
| 106 | name: "imported", disk_path: GitRepositoryService.repo_path(user.username, "imported"), |
| 107 | default_branch: "main", description: "the original" |
| 108 | ) |
| 109 | import = new_import |
| 110 | |
| 111 | described_class.perform_now(import.id) |
| 112 | import.reload |
| 113 | |
| 114 | expect(import.status).to eq("failed") |
| 115 | expect(import.error_message).to match(/already exists/) |
| 116 | expect(existing.reload.description).to eq("the original") |
| 117 | expect(user.repositories.count).to eq(1) |
| 118 | end |
| 119 | |
| 120 | it "is idempotent against double delivery (already-done import is a no-op)" do |
| 121 | import = new_import |
| 122 | described_class.perform_now(import.id) |
| 123 | described_class.perform_now(import.id) # second delivery |
| 124 | |
| 125 | expect(user.repositories.where(name: "imported").count).to eq(1) |
| 126 | expect(import.reload.status).to eq("done") |
| 127 | end |
| 128 | |
| 129 | it "clears a partial repo from a previous attempt when retrying" do |
| 130 | # Simulate a prior failed attempt that left a repo attached. |
| 131 | partial = user.repositories.create!( |
| 132 | name: "imported", disk_path: GitRepositoryService.repo_path(user.username, "imported"), |
| 133 | default_branch: "main" |
| 134 | ) |
| 135 | FileUtils.mkdir_p(partial.disk_path) |
| 136 | import = new_import(status: "failed", repository: partial) |
| 137 | |
| 138 | described_class.perform_now(import.id) |
| 139 | import.reload |
| 140 | |
| 141 | expect(import.status).to eq("done") |
| 142 | expect(Repository.where(id: partial.id)).to be_empty # old partial gone |
| 143 | expect(import.repository).to be_present |
| 144 | expect(import.repository).to be_initialized # fresh, clean import |
| 145 | expect(user.repositories.where(name: "imported").count).to eq(1) |
| 146 | end |
| 147 | end |