PageRenderTime 46ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/lib/gitlab/workhorse_spec.rb

https://gitlab.com/twang2218/gitlab
Ruby | 454 lines | 392 code | 62 blank | 0 comment | 2 complexity | 48add0cc0d810279b19fae56ae9432a0 MD5 | raw file
  1. require 'spec_helper'
  2. describe Gitlab::Workhorse do
  3. let(:project) { create(:project, :repository) }
  4. let(:repository) { project.repository }
  5. def decode_workhorse_header(array)
  6. key, value = array
  7. command, encoded_params = value.split(":")
  8. params = JSON.parse(Base64.urlsafe_decode64(encoded_params))
  9. [key, command, params]
  10. end
  11. describe ".send_git_archive" do
  12. let(:ref) { 'master' }
  13. let(:format) { 'zip' }
  14. let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path }
  15. let(:base_params) { repository.archive_metadata(ref, storage_path, format) }
  16. let(:gitaly_params) do
  17. base_params.merge(
  18. 'GitalyServer' => {
  19. 'address' => Gitlab::GitalyClient.address(project.repository_storage),
  20. 'token' => Gitlab::GitalyClient.token(project.repository_storage)
  21. },
  22. 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys
  23. )
  24. end
  25. subject do
  26. described_class.send_git_archive(repository, ref: ref, format: format)
  27. end
  28. context 'when Gitaly workhorse_archive feature is enabled' do
  29. it 'sets the header correctly' do
  30. key, command, params = decode_workhorse_header(subject)
  31. expect(key).to eq('Gitlab-Workhorse-Send-Data')
  32. expect(command).to eq('git-archive')
  33. expect(params).to include(gitaly_params)
  34. end
  35. end
  36. context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
  37. it 'sets the header correctly' do
  38. key, command, params = decode_workhorse_header(subject)
  39. expect(key).to eq('Gitlab-Workhorse-Send-Data')
  40. expect(command).to eq('git-archive')
  41. expect(params).to eq(base_params)
  42. end
  43. end
  44. context "when the repository doesn't have an archive file path" do
  45. before do
  46. allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
  47. end
  48. it "raises an error" do
  49. expect { subject }.to raise_error(RuntimeError)
  50. end
  51. end
  52. end
  53. describe '.send_git_patch' do
  54. let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
  55. subject { described_class.send_git_patch(repository, diff_refs) }
  56. context 'when Gitaly workhorse_send_git_patch feature is enabled' do
  57. it 'sets the header correctly' do
  58. key, command, params = decode_workhorse_header(subject)
  59. expect(key).to eq("Gitlab-Workhorse-Send-Data")
  60. expect(command).to eq("git-format-patch")
  61. expect(params).to eq({
  62. 'GitalyServer' => {
  63. address: Gitlab::GitalyClient.address(project.repository_storage),
  64. token: Gitlab::GitalyClient.token(project.repository_storage)
  65. },
  66. 'RawPatchRequest' => Gitaly::RawPatchRequest.new(
  67. repository: repository.gitaly_repository,
  68. left_commit_id: 'base',
  69. right_commit_id: 'head'
  70. ).to_json
  71. }.deep_stringify_keys)
  72. end
  73. end
  74. context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do
  75. it 'sets the header correctly' do
  76. key, command, params = decode_workhorse_header(subject)
  77. expect(key).to eq("Gitlab-Workhorse-Send-Data")
  78. expect(command).to eq("git-format-patch")
  79. expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
  80. end
  81. end
  82. end
  83. describe '.terminal_websocket' do
  84. def terminal(ca_pem: nil)
  85. out = {
  86. subprotocols: ['foo'],
  87. url: 'wss://example.com/terminal.ws',
  88. headers: { 'Authorization' => ['Token x'] },
  89. max_session_time: 600
  90. }
  91. out[:ca_pem] = ca_pem if ca_pem
  92. out
  93. end
  94. def workhorse(ca_pem: nil)
  95. out = {
  96. 'Terminal' => {
  97. 'Subprotocols' => ['foo'],
  98. 'Url' => 'wss://example.com/terminal.ws',
  99. 'Header' => { 'Authorization' => ['Token x'] },
  100. 'MaxSessionTime' => 600
  101. }
  102. }
  103. out['Terminal']['CAPem'] = ca_pem if ca_pem
  104. out
  105. end
  106. context 'without ca_pem' do
  107. subject { described_class.terminal_websocket(terminal) }
  108. it { is_expected.to eq(workhorse) }
  109. end
  110. context 'with ca_pem' do
  111. subject { described_class.terminal_websocket(terminal(ca_pem: "foo")) }
  112. it { is_expected.to eq(workhorse(ca_pem: "foo")) }
  113. end
  114. end
  115. describe '.send_git_diff' do
  116. let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
  117. subject { described_class.send_git_diff(repository, diff_refs) }
  118. context 'when Gitaly workhorse_send_git_diff feature is enabled' do
  119. it 'sets the header correctly' do
  120. key, command, params = decode_workhorse_header(subject)
  121. expect(key).to eq("Gitlab-Workhorse-Send-Data")
  122. expect(command).to eq("git-diff")
  123. expect(params).to eq({
  124. 'GitalyServer' => {
  125. address: Gitlab::GitalyClient.address(project.repository_storage),
  126. token: Gitlab::GitalyClient.token(project.repository_storage)
  127. },
  128. 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
  129. repository: repository.gitaly_repository,
  130. left_commit_id: 'base',
  131. right_commit_id: 'head'
  132. ).to_json
  133. }.deep_stringify_keys)
  134. end
  135. end
  136. context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do
  137. it 'sets the header correctly' do
  138. key, command, params = decode_workhorse_header(subject)
  139. expect(key).to eq("Gitlab-Workhorse-Send-Data")
  140. expect(command).to eq("git-diff")
  141. expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
  142. end
  143. end
  144. end
  145. describe ".secret" do
  146. subject { described_class.secret }
  147. before do
  148. described_class.instance_variable_set(:@secret, nil)
  149. described_class.write_secret
  150. end
  151. it 'returns 32 bytes' do
  152. expect(subject).to be_a(String)
  153. expect(subject.length).to eq(32)
  154. expect(subject.encoding).to eq(Encoding::ASCII_8BIT)
  155. end
  156. it 'accepts a trailing newline' do
  157. open(described_class.secret_path, 'a') { |f| f.write "\n" }
  158. expect(subject.length).to eq(32)
  159. end
  160. it 'raises an exception if the secret file cannot be read' do
  161. File.delete(described_class.secret_path)
  162. expect { subject }.to raise_exception(Errno::ENOENT)
  163. end
  164. it 'raises an exception if the secret file contains the wrong number of bytes' do
  165. File.truncate(described_class.secret_path, 0)
  166. expect { subject }.to raise_exception(RuntimeError)
  167. end
  168. end
  169. describe ".write_secret" do
  170. let(:secret_path) { described_class.secret_path }
  171. before do
  172. begin
  173. File.delete(secret_path)
  174. rescue Errno::ENOENT
  175. end
  176. described_class.write_secret
  177. end
  178. it 'uses mode 0600' do
  179. expect(File.stat(secret_path).mode & 0777).to eq(0600)
  180. end
  181. it 'writes base64 data' do
  182. bytes = Base64.strict_decode64(File.read(secret_path))
  183. expect(bytes).not_to be_empty
  184. end
  185. end
  186. describe '#verify_api_request!' do
  187. let(:header_key) { described_class::INTERNAL_API_REQUEST_HEADER }
  188. let(:payload) { { 'iss' => 'gitlab-workhorse' } }
  189. it 'accepts a correct header' do
  190. headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
  191. expect { call_verify(headers) }.not_to raise_error
  192. end
  193. it 'raises an error when the header is not set' do
  194. expect { call_verify({}) }.to raise_jwt_error
  195. end
  196. it 'raises an error when the header is not signed' do
  197. headers = { header_key => JWT.encode(payload, nil, 'none') }
  198. expect { call_verify(headers) }.to raise_jwt_error
  199. end
  200. it 'raises an error when the header is signed with the wrong key' do
  201. headers = { header_key => JWT.encode(payload, 'wrongkey', 'HS256') }
  202. expect { call_verify(headers) }.to raise_jwt_error
  203. end
  204. it 'raises an error when the issuer is incorrect' do
  205. payload['iss'] = 'somebody else'
  206. headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
  207. expect { call_verify(headers) }.to raise_jwt_error
  208. end
  209. def raise_jwt_error
  210. raise_error(JWT::DecodeError)
  211. end
  212. def call_verify(headers)
  213. described_class.verify_api_request!(headers)
  214. end
  215. end
  216. describe '.git_http_ok' do
  217. let(:user) { create(:user) }
  218. let(:repo_path) { repository.path_to_repo }
  219. let(:action) { 'info_refs' }
  220. let(:params) do
  221. {
  222. GL_ID: "user-#{user.id}",
  223. GL_USERNAME: user.username,
  224. GL_REPOSITORY: "project-#{project.id}",
  225. RepoPath: repo_path,
  226. ShowAllRefs: false
  227. }
  228. end
  229. subject { described_class.git_http_ok(repository, false, user, action) }
  230. it { expect(subject).to include(params) }
  231. context 'when is_wiki' do
  232. let(:params) do
  233. {
  234. GL_ID: "user-#{user.id}",
  235. GL_USERNAME: user.username,
  236. GL_REPOSITORY: "wiki-#{project.id}",
  237. RepoPath: repo_path,
  238. ShowAllRefs: false
  239. }
  240. end
  241. subject { described_class.git_http_ok(repository, true, user, action) }
  242. it { expect(subject).to include(params) }
  243. end
  244. context 'when Gitaly is enabled' do
  245. let(:gitaly_params) do
  246. {
  247. GitalyServer: {
  248. address: Gitlab::GitalyClient.address('default'),
  249. token: Gitlab::GitalyClient.token('default')
  250. }
  251. }
  252. end
  253. before do
  254. allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
  255. end
  256. it 'includes a Repository param' do
  257. repo_param = {
  258. storage_name: 'default',
  259. relative_path: project.full_path + '.git',
  260. gl_repository: "project-#{project.id}"
  261. }
  262. expect(subject[:Repository]).to include(repo_param)
  263. end
  264. context "when git_upload_pack action is passed" do
  265. let(:action) { 'git_upload_pack' }
  266. let(:feature_flag) { :post_upload_pack }
  267. it 'includes Gitaly params in the returned value' do
  268. allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(true)
  269. expect(subject).to include(gitaly_params)
  270. end
  271. context 'show_all_refs enabled' do
  272. subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
  273. it { is_expected.to include(ShowAllRefs: true) }
  274. end
  275. end
  276. context "when git_receive_pack action is passed" do
  277. let(:action) { 'git_receive_pack' }
  278. it { expect(subject).to include(gitaly_params) }
  279. end
  280. context "when info_refs action is passed" do
  281. let(:action) { 'info_refs' }
  282. it { expect(subject).to include(gitaly_params) }
  283. context 'show_all_refs enabled' do
  284. subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
  285. it { is_expected.to include(ShowAllRefs: true) }
  286. end
  287. end
  288. context 'when action passed is not supported by Gitaly' do
  289. let(:action) { 'download' }
  290. it { expect { subject }.to raise_exception('Unsupported action: download') }
  291. end
  292. end
  293. end
  294. describe '.set_key_and_notify' do
  295. let(:key) { 'test-key' }
  296. let(:value) { 'test-value' }
  297. subject { described_class.set_key_and_notify(key, value, overwrite: overwrite) }
  298. shared_examples 'set and notify' do
  299. it 'set and return the same value' do
  300. is_expected.to eq(value)
  301. end
  302. it 'set and notify' do
  303. expect_any_instance_of(::Redis).to receive(:publish)
  304. .with(described_class::NOTIFICATION_CHANNEL, "test-key=test-value")
  305. subject
  306. end
  307. end
  308. context 'when we set a new key' do
  309. let(:overwrite) { true }
  310. it_behaves_like 'set and notify'
  311. end
  312. context 'when we set an existing key' do
  313. let(:old_value) { 'existing-key' }
  314. before do
  315. described_class.set_key_and_notify(key, old_value, overwrite: true)
  316. end
  317. context 'and overwrite' do
  318. let(:overwrite) { true }
  319. it_behaves_like 'set and notify'
  320. end
  321. context 'and do not overwrite' do
  322. let(:overwrite) { false }
  323. it 'try to set but return the previous value' do
  324. is_expected.to eq(old_value)
  325. end
  326. it 'does not notify' do
  327. expect_any_instance_of(::Redis).not_to receive(:publish)
  328. subject
  329. end
  330. end
  331. end
  332. end
  333. describe '.send_git_blob' do
  334. include FakeBlobHelpers
  335. let(:blob) { fake_blob }
  336. subject { described_class.send_git_blob(repository, blob) }
  337. context 'when Gitaly workhorse_raw_show feature is enabled' do
  338. it 'sets the header correctly' do
  339. key, command, params = decode_workhorse_header(subject)
  340. expect(key).to eq('Gitlab-Workhorse-Send-Data')
  341. expect(command).to eq('git-blob')
  342. expect(params).to eq({
  343. 'GitalyServer' => {
  344. address: Gitlab::GitalyClient.address(project.repository_storage),
  345. token: Gitlab::GitalyClient.token(project.repository_storage)
  346. },
  347. 'GetBlobRequest' => {
  348. repository: repository.gitaly_repository.to_h,
  349. oid: blob.id,
  350. limit: -1
  351. }
  352. }.deep_stringify_keys)
  353. end
  354. end
  355. context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
  356. it 'sets the header correctly' do
  357. key, command, params = decode_workhorse_header(subject)
  358. expect(key).to eq('Gitlab-Workhorse-Send-Data')
  359. expect(command).to eq('git-blob')
  360. expect(params).to eq('RepoPath' => repository.path_to_repo, 'BlobId' => blob.id)
  361. end
  362. end
  363. end
  364. end