1
from mock import patch, call
5
from test_utils import CharmTestCase
6
from pymongo.errors import OperationFailure
7
from subprocess import CalledProcessError
9
# Defines a set of functions to patch on the hooks object. Any of these
10
# methods will be patched by default on the default invocations of the
11
# hooks.some_func(). Invoking the the interface change relations will cause
12
# the hooks context to be created outside of the normal mockery.
23
class MongoHooksTest(CharmTestCase):
26
super(MongoHooksTest, self).setUp(hooks, TO_PATCH)
28
# The self.config object can be used for direct invocations of the
29
# hooks methods. The side_effect of invoking the config object within
30
# the hooks object will return the value that is set in the test case's
31
# test_config dictionary
32
self.config.side_effect = self.test_config.get
33
self.relation_get.side_effect = self.test_relation.get
35
@patch.object(hooks, 'restart_mongod')
36
@patch.object(hooks, 'enable_replset')
37
# Note: patching the os.environ dictionary in-line here so there's no
38
# additional parameter sent into the function
39
@patch.dict('os.environ', JUJU_UNIT_NAME='fake-unit/0')
40
def test_replica_set_relation_joined(self, mock_enable_replset,
42
self.unit_get.return_value = 'private.address'
43
self.test_config.set('port', '1234')
44
self.test_config.set('replicaset', 'fake-replicaset')
45
self.relation_id.return_value = 'fake-relation-id'
47
mock_enable_replset.return_value = False
49
hooks.replica_set_relation_joined()
51
# Verify that mongodb was NOT restarted since the replicaset we claimed
53
self.assertFalse(mock_restart.called)
55
exp_rel_vals = {'hostname': 'private.address',
57
'replset': 'fake-replicaset',
60
# Check that the relation data was set as we expect it to be set.
61
self.relation_set.assert_called_with('fake-relation-id', exp_rel_vals)
63
mock_enable_replset.reset_mock()
64
self.relation_set.reset_mock()
65
mock_enable_replset.return_value = True
67
hooks.replica_set_relation_joined()
69
self.assertTrue(mock_restart.called)
70
self.relation_set.assert_called_with('fake-relation-id', exp_rel_vals)
72
@patch.object(hooks, 'run_admin_command')
73
@patch.object(hooks, 'Connection')
74
@patch.object(hooks, 'config')
75
@patch.object(hooks, 'mongo_client')
77
def test_init_repl_set(self, mock_sleep, mock_mongo_client_fn,
78
mock_config, mock_mongo_client,
79
mock_run_admin_command):
80
mock_mongo_client_fn.return_value = False
82
mock_config.return_value = {'replicaset': 'foo',
83
'private-address': 'mongo.local',
86
# Put the OK state (1) at the end and check the loop.
87
ret_values = [{'myState': x} for x in [0, 2, 5, 1]]
88
mock_run_admin_command.side_effect = ret_values
92
mock_run_admin_command.assert_called()
93
self.assertEqual(len(ret_values), mock_run_admin_command.call_count)
94
self.assertEqual(len(ret_values) + 1, mock_sleep.call_count)
96
mock_run_admin_command.reset_mock()
97
exc = [OperationFailure('Received replSetInitiate'),
98
OperationFailure('unhandled')]
99
mock_run_admin_command.side_effect = exc
103
self.assertTrue(False, msg="Expected error")
104
except OperationFailure:
107
mock_run_admin_command.assert_called()
108
self.assertEqual(2, mock_run_admin_command.call_count)
110
@patch.object(hooks, 'mongo_client_smart')
111
def test_join_replset(self, mock_mongo_client):
113
self.assertFalse(mock_mongo_client.called)
115
mock_mongo_client.reset_mock()
116
hooks.join_replset(master_node='mongo.local')
117
self.assertFalse(mock_mongo_client.called)
119
mock_mongo_client.reset_mock()
120
hooks.join_replset(host='fake-host')
121
self.assertFalse(mock_mongo_client.called)
123
mock_mongo_client.reset_mock()
124
hooks.join_replset(master_node='mongo.local', host='fake-host')
125
mock_mongo_client.assert_called_with('localhost',
126
'rs.add("fake-host")')
128
@patch.object(hooks, 'mongo_client')
129
def test_leave_replset(self, mock_mongo_client):
130
hooks.leave_replset()
131
self.assertFalse(mock_mongo_client.called)
133
mock_mongo_client.reset_mock()
134
hooks.leave_replset(master_node='mongo.local')
135
self.assertFalse(mock_mongo_client.called)
137
mock_mongo_client.reset_mock()
138
hooks.leave_replset(host='fake-host')
139
self.assertFalse(mock_mongo_client.called)
141
mock_mongo_client.reset_mock()
142
hooks.leave_replset('mongo.local', 'fake-host')
143
mock_mongo_client.assert_called_with('mongo.local',
144
'rs.remove("fake-host")')
146
@patch.object(hooks, 'apt_install')
147
@patch.object(hooks, 'apt_update')
148
@patch.object(hooks, 'add_source')
149
@patch.dict('os.environ', CHARM_DIR='/tmp/charm/dir')
150
def test_install_hook(self, mock_add_source, mock_apt_update,
152
self.test_config.set('source', 'fake-source')
153
self.test_config.set('key', 'fake-key')
156
mock_add_source.assert_called_with('fake-source', 'fake-key')
157
mock_apt_update.assert_called_with(fatal=True)
158
mock_apt_install.assert_called_with(packages=hooks.INSTALL_PACKAGES,
161
@patch.object(hooks, 'run_admin_command')
162
@patch.object(hooks, 'Connection')
164
def test_am_i_primary(self, mock_sleep, mock_mongo_client,
166
mock_run_admin_cmd.side_effect = [{'myState': x} for x in xrange(5)]
167
expected_results = [True if x == 1 else False for x in xrange(5)]
169
# Check expected return values each time...
170
for exp in expected_results:
171
rv = hooks.am_i_primary()
172
self.assertEqual(exp, rv)
174
@patch.object(hooks, 'run_admin_command')
175
@patch.object(hooks, 'Connection')
177
def test_am_i_primary_too_many_attempts(self, mock_sleep,
180
msg = 'replSetInitiate - should come online shortly'
181
mock_run_admin_cmd.side_effect = [OperationFailure(msg)
186
self.assertTrue(False, 'Expected failure.')
187
except hooks.TimeoutException:
188
self.assertEqual(mock_run_admin_cmd.call_count, 10)
191
@patch.object(hooks, 'run_admin_command')
192
@patch.object(hooks, 'Connection')
194
def test_am_i_primary_operation_failures(self, mock_sleep,
197
mock_run_admin_cmd.side_effect = OperationFailure('EMPTYCONFIG')
199
rv = hooks.am_i_primary()
200
mock_run_admin_cmd.assert_called()
203
mock_run_admin_cmd.reset_mock()
204
mock_run_admin_cmd.side_effect = OperationFailure('unexpected failure')
207
self.assertFalse(True, "Expected OperationFailure to be raised")
208
except OperationFailure:
209
mock_run_admin_cmd.assert_called()
212
@patch('subprocess.check_output')
213
def test_mongo_client_smart_no_command(self, mock_check_output,
215
rv = hooks.mongo_client_smart()
217
self.assertEqual(0, mock_check_output.call_count)
219
mock_check_output.reset_mock()
220
mock_check_output.return_value = '{"ok": 1}'
222
rv = hooks.mongo_client_smart(command='fake-cmd')
224
mock_check_output.assert_called_once_with(['mongo', '--quiet',
225
'--host', 'localhost',
227
'printjson(fake-cmd)'])
230
@patch('subprocess.check_output')
231
def test_mongo_client_smart_error_cases(self, mock_ck_output, mock_sleep):
232
mock_ck_output.side_effect = [CalledProcessError(1, 'cmd',
235
rv = hooks.mongo_client_smart(command='fake-cmd')
238
@patch('subprocess.call')
239
def test_mongo_client(self, mock_subprocess):
240
rv = hooks.mongo_client()
242
self.assertEqual(0, mock_subprocess.call_count)
244
mock_subprocess.reset_mock()
245
rv = hooks.mongo_client(host='fake-host')
247
self.assertEqual(0, mock_subprocess.call_count)
249
mock_subprocess.reset_mock()
250
rv = hooks.mongo_client(command='fake-command')
252
self.assertEqual(0, mock_subprocess.call_count)
254
mock_subprocess.reset_mock()
255
mock_subprocess.return_value = 0
256
rv = hooks.mongo_client(host='fake-host', command='fake-command')
257
expected_cmd = ("mongo --host %s --eval 'printjson(%s)'"
258
% ('fake-host', 'fake-command'))
259
mock_subprocess.assert_called_once_with(expected_cmd, shell=True)
262
mock_subprocess.reset_mock()
263
mock_subprocess.return_value = 1
264
rv = hooks.mongo_client(host='fake-host', command='fake-command')
265
expected_cmd = ("mongo --host %s --eval 'printjson(%s)'"
266
% ('fake-host', 'fake-command'))
267
mock_subprocess.assert_called_once_with(expected_cmd, shell=True)
270
@patch.object(hooks, 'am_i_primary')
271
@patch.object(hooks, 'init_replset')
272
@patch.object(hooks, 'relation_get')
273
@patch.object(hooks, 'peer_units')
274
@patch.object(hooks, 'oldest_peer')
275
@patch.object(hooks, 'join_replset')
276
@patch.object(hooks, 'unit_get')
277
def test_replica_set_relation_changed(self, mock_unit_get,
278
mock_join_replset, mock_oldest_peer,
279
mock_peer_units, mock_relation_get,
280
mock_init_replset, mock_is_primary):
281
# set the unit_get('private-address')
282
mock_unit_get.return_value = 'juju-local-unit-0.local'
283
mock_relation_get.return_value = None
285
# Test when remote hostname is None, should not join
286
hooks.replica_set_relation_changed()
287
self.assertEqual(0, mock_join_replset.call_count)
289
# Test remote hostname is valid, but master is somehow not defined
290
mock_join_replset.reset_mock()
291
mock_relation_get.return_value = 'juju-local-unit-0'
293
hooks.replica_set_relation_changed()
295
self.assertEqual(1, mock_join_replset.call_count)
297
# Test when not oldest peer, don't init replica set
298
mock_init_replset.reset_mock()
299
mock_oldest_peer.reset_mock()
300
mock_peer_units.return_value = ['mongodb/1', 'mongodb/2']
301
mock_oldest_peer.return_value = False
303
hooks.replica_set_relation_changed()
305
self.assertEqual(mock_init_replset.call_count, 0)
307
# Test when its also the PRIMARY
308
mock_relation_get.reset_mock()
309
mock_relation_get.side_effect = ['juju-remote-unit-0', '12345']
310
mock_oldest_peer.reset_mock()
311
mock_oldest_peer.return_value = False
312
mock_is_primary.reset_mock()
313
mock_is_primary.return_value = True
315
hooks.replica_set_relation_changed()
316
call1 = call('juju-local-unit-0.local:27017',
317
'juju-remote-unit-0:12345')
318
mock_join_replset.assert_has_calls(call1)
320
@patch.object(hooks, 'unit_get')
321
@patch.object(hooks, 'leave_replset')
322
@patch.object(hooks, 'am_i_primary')
323
def test_replica_set_relation_departed(self, mock_am_i_primary,
324
mock_leave_replset, mock_unit_get):
325
mock_am_i_primary.return_value = False
326
hooks.replica_set_relation_departed()
328
self.assertEqual(0, mock_leave_replset.call_count)
330
mock_am_i_primary.reset_mock()
331
mock_am_i_primary.return_value = True
332
mock_unit_get.return_value = 'juju-local'
334
self.test_relation.set({'hostname': 'juju-remote',
336
mock_leave_replset.reset_mock()
338
hooks.replica_set_relation_departed()
340
call1 = call('juju-local:27017', 'juju-remote:27017')
341
mock_leave_replset.assert_has_calls(call1)