main
rb 147 lines 5.34 KB
Raw
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