beaker-google 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/release.yml +41 -0
  3. data/.github/workflows/ci.yml +30 -17
  4. data/.github/workflows/release.yml +91 -17
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +4 -518
  7. data/.rubocop_todo.yml +20 -0
  8. data/.simplecov +2 -0
  9. data/CHANGELOG.md +50 -1
  10. data/Gemfile +13 -2
  11. data/README.md +2 -2
  12. data/Rakefile +23 -6
  13. data/beaker-google.gemspec +27 -23
  14. data/bin/beaker-google +1 -0
  15. data/lib/beaker/hypervisor/google.rb +5 -1
  16. data/lib/beaker/hypervisor/google_compute.rb +55 -25
  17. data/lib/beaker/hypervisor/google_compute_helper.rb +599 -581
  18. data/lib/beaker-google/version.rb +3 -1
  19. data/spec/classes/beaker/google_compute_constants_and_defaults_spec.rb +141 -0
  20. data/spec/classes/beaker/google_compute_helper_api_version_spec.rb +37 -0
  21. data/spec/classes/beaker/google_compute_helper_configuration_validation_spec.rb +100 -0
  22. data/spec/classes/beaker/google_compute_helper_constants_and_defaults_spec.rb +66 -0
  23. data/spec/classes/beaker/google_compute_helper_edge_cases_spec.rb +173 -0
  24. data/spec/classes/beaker/google_compute_helper_error_handling_spec.rb +105 -0
  25. data/spec/classes/beaker/google_compute_helper_spec.rb +368 -0
  26. data/spec/classes/beaker/google_compute_helper_utilities_spec.rb +95 -0
  27. data/spec/classes/beaker/google_compute_initialization_spec.rb +52 -0
  28. data/spec/classes/beaker/google_compute_provision_spec.rb +68 -0
  29. data/spec/classes/beaker/google_compute_spec.rb +95 -0
  30. data/spec/classes/beaker/google_compute_ssh_key_management_spec.rb +115 -0
  31. data/spec/classes/beaker/google_spec.rb +68 -0
  32. data/spec/classes/beaker-google/beaker_google_version_spec.rb +19 -0
  33. data/spec/classes/beaker_integration_spec.rb +67 -0
  34. data/spec/spec_helper.rb +45 -0
  35. data/spec/support/google_compute_shared_setup.rb +138 -0
  36. data/spec/support/shared_mocks.rb +89 -0
  37. metadata +102 -38
  38. data/.github/workflows/codeql-analysis.yml +0 -70
@@ -0,0 +1,368 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add mutex_m requirement for Ruby 3.4.0 compatibility
4
+ begin
5
+ require 'mutex_m'
6
+ rescue LoadError
7
+ # mutex_m is not available on all Ruby versions
8
+ end
9
+
10
+ # Require core dependencies
11
+ require 'rspec'
12
+ require 'rspec/its'
13
+ require 'fakefs/spec_helpers'
14
+
15
+ # Require beaker first to establish the base classes
16
+ require 'beaker'
17
+
18
+ # Require the actual implementation before mocks to avoid conflicts
19
+ require 'beaker/hypervisor/google_compute_helper'
20
+
21
+ # Load shared mock classes
22
+ require File.join(File.dirname(__FILE__), '../../support/shared_mocks')
23
+
24
+ RSpec.describe Beaker::GoogleComputeHelper do
25
+ subject(:google_compute_helper) { described_class.new(options) }
26
+
27
+ let(:options) do
28
+ {
29
+ gce_project: 'test-project',
30
+ gce_zone: 'us-central1-a',
31
+ gce_network: 'default',
32
+ gce_ports: ['80/tcp', '443/tcp'],
33
+ logger: logger,
34
+ }
35
+ end
36
+
37
+ let(:logger) { instance_double(Beaker::Logger, debug: nil, info: nil, warn: nil, error: nil) }
38
+
39
+ # Stub Google API classes to avoid actual authentication
40
+ before do
41
+ client_options = instance_double(Google::Apis::ClientOptions)
42
+ allow(client_options).to receive(:application_name=)
43
+ allow(client_options).to receive(:application_version=)
44
+ allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
45
+
46
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:from_env).and_return(
47
+ instance_double(Google::Auth::ServiceAccountCredentials),
48
+ )
49
+ allow(Google::Auth).to receive(:get_application_default).and_return(
50
+ instance_double(Google::Auth::ServiceAccountCredentials),
51
+ )
52
+
53
+ compute_service = instance_double(Google::Apis::ComputeV1::ComputeService)
54
+ allow(compute_service).to receive(:authorization=)
55
+ allow(compute_service).to receive(:request_options).and_return(Google::Apis::RequestOptions.new)
56
+ allow(Google::Apis::ComputeV1::ComputeService).to receive(:new).and_return(compute_service)
57
+
58
+ oslogin_service = instance_double(Google::Apis::OsloginV1::CloudOSLoginService)
59
+ allow(oslogin_service).to receive(:authorization=)
60
+ allow(Google::Apis::OsloginV1::CloudOSLoginService).to receive(:new).and_return(oslogin_service)
61
+
62
+ allow(ENV).to receive(:[]).and_call_original
63
+ allow(ENV).to receive(:fetch).and_call_original
64
+ allow(ENV).to receive(:[]).with('GOOGLE_CLOUD_UNIVERSE_DOMAIN').and_return(nil)
65
+ allow(ENV).to receive(:[]).with('GOOGLE_CLOUD_PROJECT').and_return(nil)
66
+ allow(ENV).to receive(:[]).with('GCLOUD_PROJECT').and_return(nil)
67
+ allow(ENV).to receive(:[]).with('GOOGLE_APPLICATION_CREDENTIALS').and_return(nil)
68
+ end
69
+
70
+ describe '#initialize' do
71
+ context 'with environment variables' do
72
+ before do
73
+ allow(ENV).to receive(:[]).with('BEAKER_gce_project').and_return('env-project')
74
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_project', nil).and_return('env-project')
75
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_zone', 'us-central1-a').and_return('us-west1-a')
76
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_network', 'default').and_return('custom-network')
77
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_subnetwork', nil).and_return('custom-subnet')
78
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_ports', '').and_return('22/tcp,80/tcp')
79
+ end
80
+
81
+ it 'uses environment variables when available' do
82
+ helper = described_class.new(options.merge(gce_project: nil))
83
+ expect(helper.instance_variable_get(:@options)[:gce_project]).to eq('env-project')
84
+ end
85
+ end
86
+
87
+ context 'without required project' do
88
+ before do
89
+ allow(ENV).to receive(:[]).with('BEAKER_gce_project').and_return(nil)
90
+ allow(ENV).to receive(:fetch).with('BEAKER_gce_project', nil).and_return(nil)
91
+ end
92
+
93
+ it 'raises an error when gce_project is not provided' do
94
+ expect do
95
+ described_class.new(options.merge(gce_project: nil))
96
+ end.to raise_error('You must specify a gce_project for Google Compute Engine instances!')
97
+ end
98
+ end
99
+
100
+ context 'with invalid ports' do
101
+ it 'raises an error for invalid port format' do
102
+ expect do
103
+ described_class.new(options.merge(gce_ports: ['invalid-port']))
104
+ end.to raise_error(/Invalid format for port/)
105
+ end
106
+
107
+ it 'raises an error for invalid protocol' do
108
+ expect do
109
+ described_class.new(options.merge(gce_ports: ['80/invalid']))
110
+ end.to raise_error(/Invalid value 'invalid' for protocol/)
111
+ end
112
+ end
113
+ end
114
+
115
+ describe '#client' do
116
+ it 'sets the application name on Google API client options' do
117
+ client_options = instance_spy(Google::Apis::ClientOptions)
118
+ allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
119
+
120
+ google_compute_helper.send(:client, Beaker::Version::STRING)
121
+
122
+ expect(client_options).to have_received(:application_name=).with('beaker-google').at_least(:once)
123
+ end
124
+
125
+ it 'sets the application version on Google API client options' do
126
+ client_options = instance_spy(Google::Apis::ClientOptions)
127
+ allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
128
+
129
+ google_compute_helper.send(:client, Beaker::Version::STRING)
130
+
131
+ expect(client_options).to have_received(:application_version=).with(Beaker::Version::STRING).at_least(:once)
132
+ end
133
+ end
134
+
135
+ describe '#authenticate' do
136
+ context 'when GOOGLE_APPLICATION_CREDENTIALS environment variable is set' do
137
+ let(:auth_double) { instance_double(Google::Auth::ServiceAccountCredentials) }
138
+
139
+ before do
140
+ allow(ENV).to receive(:[]).with('GOOGLE_APPLICATION_CREDENTIALS').and_return('/path/to/credentials.json')
141
+ allow(Google::Auth::ServiceAccountCredentials).to receive(:from_env)
142
+ .with(scope: described_class::GCP_AUTH_SCOPE)
143
+ .and_return(auth_double)
144
+ end
145
+
146
+ it 'uses service account credentials from environment' do
147
+ # Create a fresh instance to test authentication
148
+ fresh_instance = described_class.allocate
149
+ result = fresh_instance.send(:authenticate)
150
+ expect(result).to eq(auth_double)
151
+ end
152
+ end
153
+
154
+ context 'when GOOGLE_APPLICATION_CREDENTIALS is not set' do
155
+ let(:auth_double) { instance_double(Google::Auth::ServiceAccountCredentials) }
156
+
157
+ before do
158
+ allow(ENV).to receive(:[]).with('GOOGLE_APPLICATION_CREDENTIALS').and_return(nil)
159
+ allow(Google::Auth).to receive(:get_application_default)
160
+ .with(described_class::GCP_AUTH_SCOPE)
161
+ .and_return(auth_double)
162
+ end
163
+
164
+ it 'falls back to application default credentials' do
165
+ # Create a fresh instance to test authentication
166
+ fresh_instance = described_class.allocate
167
+ result = fresh_instance.send(:authenticate)
168
+ expect(result).to eq(auth_double)
169
+ end
170
+ end
171
+ end
172
+
173
+ describe '#default_zone' do
174
+ it 'returns the configured zone from options' do
175
+ expect(google_compute_helper.default_zone).to eq(options[:gce_zone])
176
+ end
177
+ end
178
+
179
+ describe '#default_region' do
180
+ it 'extracts region from zone in options' do
181
+ expected_region = options[:gce_zone].split('-')[0..1].join('-')
182
+ expect(google_compute_helper.default_region).to eq(expected_region)
183
+ end
184
+ end
185
+
186
+ describe '#default_network' do
187
+ it 'returns the configured network from options' do
188
+ expect(google_compute_helper.default_network).to eq(options[:gce_network])
189
+ end
190
+ end
191
+
192
+ describe 'API methods' do
193
+ let(:compute_service) { instance_double(Google::Apis::ComputeV1::ComputeService) }
194
+
195
+ before do
196
+ google_compute_helper.instance_variable_set(:@compute, compute_service)
197
+ end
198
+
199
+ describe '#get_network' do
200
+ it 'retrieves network information' do
201
+ allow(compute_service).to receive(:get_network).and_return(MockNetwork.new)
202
+ expect(google_compute_helper.get_network).to be_a(MockNetwork)
203
+ end
204
+ end
205
+
206
+ describe '#get_machine_type' do
207
+ it 'retrieves machine type information' do
208
+ allow(compute_service).to receive(:get_machine_type).and_return(MockMachineType.new)
209
+ expect(google_compute_helper.get_machine_type('n1-standard-1')).to be_a(MockMachineType)
210
+ end
211
+ end
212
+
213
+ describe '#get_image' do
214
+ it 'retrieves image information' do
215
+ allow(compute_service).to receive(:get_image).and_return(MockImage.new)
216
+ expect(google_compute_helper.get_image('test-project', 'test-image')).to be_a(MockImage)
217
+ end
218
+ end
219
+
220
+ describe '#get_latest_image_from_family' do
221
+ before do
222
+ allow(compute_service).to receive(:get_image_from_family).and_return(MockImage.new)
223
+ end
224
+
225
+ it 'retrieves latest image from family' do
226
+ expect(google_compute_helper.get_latest_image_from_family('test-project', 'test-family')).to be_a(MockImage)
227
+ end
228
+ end
229
+
230
+ describe '#create_firewall' do
231
+ let(:network) { instance_double(Google::Apis::ComputeV1::Network, self_link: 'network-link') }
232
+ let(:result) do
233
+ google_compute_helper.create_firewall(
234
+ 'test-firewall',
235
+ network,
236
+ allow: ['80/tcp'],
237
+ source_ranges: ['0.0.0.0/0'],
238
+ target_tags: ['test-tag'],
239
+ )
240
+ end
241
+
242
+ before do
243
+ allow(compute_service).to receive_messages(insert_firewall: MockOperation.new,
244
+ wait_global_operation: MockOperation.new)
245
+ end
246
+
247
+ it 'creates a firewall rule' do
248
+ expect(result).to be_a(MockOperation)
249
+ end
250
+ end
251
+
252
+ describe '#delete_firewall' do
253
+ let(:result) { google_compute_helper.delete_firewall('test-firewall') }
254
+
255
+ before do
256
+ allow(compute_service).to receive_messages(delete_firewall: MockOperation.new,
257
+ wait_global_operation: MockOperation.new)
258
+ end
259
+
260
+ it 'deletes a firewall rule' do
261
+ expect(result).to be_a(MockOperation)
262
+ end
263
+ end
264
+
265
+ describe '#create_instance' do
266
+ let(:image) { instance_double(Google::Apis::ComputeV1::Image, self_link: 'image-link') }
267
+ let(:machine_type) { instance_double(Google::Apis::ComputeV1::MachineType, self_link: 'machine-link') }
268
+
269
+ before do
270
+ allow(compute_service).to receive_messages(
271
+ get_network: MockNetwork.new,
272
+ get_subnetwork: MockSubnetwork.new,
273
+ insert_instance: MockOperation.new,
274
+ wait_zone_operation: MockOperation.new,
275
+ )
276
+ end
277
+
278
+ it 'creates an instance' do
279
+ result = google_compute_helper.create_instance('test-instance', image, machine_type, 20, 'test-host')
280
+ expect(result).to be_a(MockOperation)
281
+ end
282
+ end
283
+
284
+ describe '#delete_instance' do
285
+ it 'deletes an instance' do
286
+ allow(compute_service).to receive_messages(delete_instance: MockOperation.new,
287
+ wait_zone_operation: MockOperation.new)
288
+ result = google_compute_helper.delete_instance('test-instance')
289
+ expect(result).to be_a(MockOperation)
290
+ end
291
+ end
292
+
293
+ describe '#get_instance' do
294
+ it 'retrieves instance information' do
295
+ allow(compute_service).to receive(:get_instance).and_return(MockInstance.new)
296
+ result = google_compute_helper.get_instance('test-instance')
297
+ expect(result).to be_a(MockInstance)
298
+ end
299
+ end
300
+
301
+ describe '#add_instance_tag' do
302
+ before do
303
+ allow(compute_service).to receive_messages(
304
+ get_instance: MockInstance.new,
305
+ set_instance_tags: MockOperation.new,
306
+ wait_zone_operation: MockOperation.new,
307
+ )
308
+ end
309
+
310
+ it 'adds a tag to an instance' do
311
+ result = google_compute_helper.add_instance_tag('test-instance', 'new-tag')
312
+ expect(result).to be_a(MockOperation)
313
+ end
314
+ end
315
+
316
+ describe '#set_metadata_on_instance' do
317
+ let(:metadata) { [{ key: 'test-key', value: 'test-value' }] }
318
+
319
+ before do
320
+ allow(compute_service).to receive_messages(
321
+ get_instance: MockInstance.new,
322
+ set_instance_metadata: MockOperation.new,
323
+ wait_zone_operation: MockOperation.new,
324
+ )
325
+ end
326
+
327
+ it 'sets metadata on an instance' do
328
+ result = google_compute_helper.set_metadata_on_instance('test-instance', metadata)
329
+ expect(result).to be_a(MockOperation)
330
+ end
331
+ end
332
+ end
333
+
334
+ describe 'error handling' do
335
+ describe 'GoogleComputeError' do
336
+ it 'defines a custom error class' do
337
+ expect(Beaker::GoogleComputeHelper::GoogleComputeError).to be < StandardError
338
+ end
339
+ end
340
+ end
341
+
342
+ describe 'constants' do
343
+ it 'defines AUTH_URL constant' do
344
+ expect(described_class::AUTH_URL).to eq('https://www.googleapis.com/auth/compute')
345
+ end
346
+
347
+ it 'defines API_VERSION constant' do
348
+ expect(described_class::API_VERSION).to eq('v1')
349
+ end
350
+
351
+ it 'defines DEFAULT_ZONE_NAME constant' do
352
+ expect(described_class::DEFAULT_ZONE_NAME).to eq('us-central1-a')
353
+ end
354
+
355
+ it 'defines DEFAULT_MACHINE_TYPE constant' do
356
+ expect(described_class::DEFAULT_MACHINE_TYPE).to eq('e2-standard-4')
357
+ end
358
+
359
+ it 'defines DEFAULT_NETWORK_NAME constant' do
360
+ expect(described_class::DEFAULT_NETWORK_NAME).to eq('default')
361
+ end
362
+
363
+ it 'defines VALID_PROTOS constant' do
364
+ expect(described_class::VALID_PROTOS).to eq(%w[tcp udp icmp esp ah ipip sctp])
365
+ .and be_frozen
366
+ end
367
+ end
368
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Beaker::GoogleComputeHelper do
6
+ context 'when using utilities' do
7
+ let(:logger) { instance_double(Beaker::Logger) }
8
+ let(:base_options) { { logger: logger, gce_project: 'test-project' } }
9
+ let(:helper) { described_class.new(base_options.merge(gce_zone: 'us-central1-a')) }
10
+
11
+ before do
12
+ # Mock the authentication and service initialization
13
+ allow(Google::Auth).to receive(:get_application_default).and_return(double)
14
+
15
+ compute_service = double
16
+ allow(compute_service).to receive(:authorization=)
17
+ allow(compute_service).to receive(:request_options).and_return(Google::Apis::RequestOptions.new)
18
+ allow(Google::Apis::ComputeV1::ComputeService).to receive(:new).and_return(compute_service)
19
+
20
+ oslogin_service = double
21
+ allow(oslogin_service).to receive(:authorization=)
22
+ allow(Google::Apis::OsloginV1::CloudOSLoginService).to receive(:new).and_return(oslogin_service)
23
+
24
+ client_options = double
25
+ allow(client_options).to receive(:application_name=)
26
+ allow(client_options).to receive(:application_version=)
27
+ allow(Google::Apis::ClientOptions).to receive(:default).and_return(client_options)
28
+ end
29
+
30
+ describe 'region extraction' do
31
+ let(:zone_test_cases) do
32
+ {
33
+ 'us-central1-a' => 'us-central1',
34
+ 'us-west1-b' => 'us-west1',
35
+ 'europe-west1-c' => 'europe-west1',
36
+ 'asia-east1-a' => 'asia-east1',
37
+ }
38
+ end
39
+
40
+ it 'extracts region from zone using default_region method' do
41
+ expect(helper.default_region).to eq('us-central1')
42
+ end
43
+
44
+ it 'handles different zone formats' do
45
+ zone_test_cases.each do |zone, expected_region|
46
+ helper_with_zone = described_class.allocate
47
+ helper_with_zone.instance_variable_set(:@options, base_options.merge(gce_zone: zone))
48
+ expect(helper_with_zone.send(:default_region)).to eq(expected_region)
49
+ end
50
+ end
51
+ end
52
+
53
+ describe 'connection preferences' do
54
+ it 'prefers IP connections' do
55
+ google_compute = Beaker::GoogleCompute.new([], base_options)
56
+ expect(google_compute.connection_preference(nil)).to eq([:ip])
57
+ end
58
+ end
59
+
60
+ describe 'environment variable handling' do
61
+ it 'uses default zone when environment variable is not set' do
62
+ # Test the actual default zone constant
63
+ expect(described_class::DEFAULT_ZONE_NAME).to eq('us-central1-a')
64
+ end
65
+
66
+ it 'uses default network when environment variable is not set' do
67
+ # Test the actual default network constant
68
+ expect(described_class::DEFAULT_NETWORK_NAME).to eq('default')
69
+ end
70
+
71
+ it 'processes port configuration through actual gem logic' do
72
+ helper_with_ports = described_class.new(
73
+ base_options.merge(gce_ports: ['80/tcp', '443/tcp', '22/tcp']),
74
+ )
75
+ expect(helper_with_ports.options[:gce_ports]).to eq(['80/tcp', '443/tcp', '22/tcp'])
76
+ end
77
+
78
+ it 'handles empty port configuration through actual gem logic' do
79
+ helper_with_empty_ports = described_class.new(base_options.merge(gce_ports: []))
80
+ expect(helper_with_empty_ports.options[:gce_ports]).to eq([])
81
+ end
82
+ end
83
+
84
+ describe 'Windows image detection' do
85
+ it 'identifies Windows images correctly' do
86
+ expect(Beaker::GoogleCompute::WINDOWS_IMAGE_PROJECT).to include('windows-cloud')
87
+ .and include('windows-sql-cloud')
88
+ end
89
+
90
+ it 'excludes non-Windows images' do
91
+ expect(Beaker::GoogleCompute::WINDOWS_IMAGE_PROJECT).not_to include('ubuntu-os-cloud')
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative '../../support/google_compute_shared_setup'
5
+
6
+ RSpec.describe Beaker::GoogleCompute do
7
+ include GoogleComputeSharedSetup
8
+
9
+ setup_google_compute_mocks
10
+ setup_test_hosts
11
+ setup_test_options
12
+
13
+ subject(:google_compute) { described_class.new(hosts, options) }
14
+
15
+ describe '#initialize' do
16
+ it 'initializes hosts' do
17
+ expect(google_compute.instance_variable_get(:@hosts)).to eq(hosts)
18
+ end
19
+
20
+ it 'initializes options' do
21
+ expect(google_compute.instance_variable_get(:@options)).to eq(options)
22
+ end
23
+
24
+ it 'initializes logger' do
25
+ expect(google_compute.instance_variable_get(:@logger)).to eq(logger)
26
+ end
27
+
28
+ it 'initializes external firewall name' do
29
+ expect(google_compute.instance_variable_get(:@external_firewall_name)).to eq('')
30
+ end
31
+
32
+ it 'initializes internal firewall name' do
33
+ expect(google_compute.instance_variable_get(:@internal_firewall_name)).to eq('')
34
+ end
35
+ end
36
+
37
+ describe '#connection_preference' do
38
+ it 'returns IP preference' do
39
+ expect(google_compute.connection_preference(primary_host)).to eq([:ip])
40
+ end
41
+ end
42
+
43
+ describe 'constants' do
44
+ it 'defines SLEEPWAIT constant' do
45
+ expect(described_class::SLEEPWAIT).to eq(5)
46
+ end
47
+
48
+ it 'defines WINDOWS_IMAGE_PROJECT constant' do
49
+ expect(described_class::WINDOWS_IMAGE_PROJECT).to eq(%w[windows-cloud windows-sql-cloud])
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative '../../support/google_compute_shared_setup'
5
+
6
+ RSpec.describe Beaker::GoogleCompute do
7
+ include GoogleComputeSharedSetup
8
+
9
+ setup_google_compute_mocks
10
+ setup_test_hosts
11
+ setup_test_options
12
+
13
+ subject(:google_compute) { described_class.new(hosts, options) }
14
+
15
+ describe '#provision' do
16
+ include FakeFS::SpecHelpers
17
+
18
+ before do
19
+ allow(SecureRandom).to receive(:hex).and_return('abcd1234')
20
+
21
+ # Set up fake SSH keys for provision tests
22
+ private_key_path = File.join(Dir.home, '.ssh', 'google_compute_engine')
23
+ public_key_path = "#{private_key_path}.pub"
24
+
25
+ FileUtils.mkdir_p(File.dirname(private_key_path))
26
+ File.write(private_key_path, 'private key content')
27
+ File.write(public_key_path, 'ssh-rsa AAAAB3NzaC1yc2E...')
28
+
29
+ # Mock external SSH operations that are inherited from parent class
30
+ # These are only needed for provision tests and perform external SSH operations
31
+ # rubocop:disable RSpec/SubjectStub
32
+ allow(google_compute).to receive(:copy_ssh_to_root)
33
+ allow(google_compute).to receive(:enable_root_login)
34
+ # rubocop:enable RSpec/SubjectStub
35
+ end
36
+
37
+ it 'provisions all hosts successfully' do
38
+ expect { google_compute.provision }.not_to raise_error
39
+ end
40
+
41
+ it 'sets vmhostname for all hosts' do
42
+ google_compute.provision
43
+ hosts.each do |host|
44
+ expect(host['vmhostname']).to match(/beaker-abcd1234-.*/)
45
+ end
46
+ end
47
+
48
+ it 'sets IP address for all hosts' do
49
+ google_compute.provision
50
+ hosts.each do |host|
51
+ expect(host['ip']).to eq('1.2.3.4')
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#cleanup' do
57
+ before do
58
+ google_compute.instance_variable_set(:@external_firewall_name, 'external-fw')
59
+ google_compute.instance_variable_set(:@internal_firewall_name, 'internal-fw')
60
+ primary_host['vmhostname'] = 'vm-host1'
61
+ secondary_host['vmhostname'] = 'vm-host2'
62
+ end
63
+
64
+ it 'cleans up resources' do
65
+ expect { google_compute.cleanup }.not_to raise_error
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative '../../support/google_compute_shared_setup'
5
+
6
+ RSpec.describe Beaker::GoogleCompute do
7
+ include GoogleComputeSharedSetup
8
+
9
+ setup_google_compute_mocks
10
+ setup_test_hosts
11
+ setup_test_options
12
+
13
+ subject(:google_compute) { described_class.new(hosts, options) }
14
+
15
+ describe '#find_google_ssh_private_key' do
16
+ include FakeFS::SpecHelpers
17
+
18
+ let(:default_private_key) { File.join(Dir.home, '.ssh', 'google_compute_engine') }
19
+ let(:custom_private_key) { '/custom/path/to/key' }
20
+
21
+ context 'when default key exists' do
22
+ before do
23
+ FileUtils.mkdir_p(File.dirname(default_private_key))
24
+ File.write(default_private_key, 'private key content')
25
+ end
26
+
27
+ it 'returns the default private key path' do
28
+ expect(google_compute.find_google_ssh_private_key).to eq(default_private_key)
29
+ end
30
+
31
+ it 'sets the private key in options' do
32
+ google_compute.find_google_ssh_private_key
33
+ expect(google_compute.instance_variable_get(:@options)[:gce_ssh_private_key]).to eq(default_private_key)
34
+ end
35
+ end
36
+
37
+ context 'when custom key is specified in options' do
38
+ before do
39
+ FileUtils.mkdir_p(File.dirname(custom_private_key))
40
+ File.write(custom_private_key, 'private key content')
41
+ google_compute.instance_variable_get(:@options)[:gce_ssh_private_key] = custom_private_key
42
+ end
43
+
44
+ it 'returns the custom private key path' do
45
+ expect(google_compute.find_google_ssh_private_key).to eq(custom_private_key)
46
+ end
47
+ end
48
+
49
+ context 'when private key does not exist' do
50
+ it 'raises an error' do
51
+ expect { google_compute.find_google_ssh_private_key }.to raise_error(/Could not find GCE Private SSH key/)
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#find_google_ssh_public_key' do
57
+ include FakeFS::SpecHelpers
58
+
59
+ let(:private_key) { '/path/to/private_key' }
60
+ let(:public_key) { '/path/to/private_key.pub' }
61
+
62
+ before do
63
+ FileUtils.mkdir_p(File.dirname(private_key))
64
+ File.write(private_key, 'private key content')
65
+ end
66
+
67
+ context 'when public key exists' do
68
+ before do
69
+ File.write(public_key, 'public key content')
70
+ # Set up the private key path in options for this test
71
+ google_compute.instance_variable_get(:@options)[:gce_ssh_private_key] = private_key
72
+ end
73
+
74
+ it 'returns the public key path' do
75
+ expect(google_compute.find_google_ssh_public_key).to eq(public_key)
76
+ end
77
+
78
+ it 'sets the public key in options' do
79
+ google_compute.find_google_ssh_public_key
80
+ expect(google_compute.instance_variable_get(:@options)[:gce_ssh_public_key]).to eq(public_key)
81
+ end
82
+ end
83
+
84
+ context 'when public key does not exist' do
85
+ before do
86
+ # Set up the private key path in options for this test
87
+ google_compute.instance_variable_get(:@options)[:gce_ssh_private_key] = private_key
88
+ end
89
+
90
+ it 'raises an error' do
91
+ expect { google_compute.find_google_ssh_public_key }.to raise_error(/Could not find GCE Public SSH key/)
92
+ end
93
+ end
94
+ end
95
+ end