1
# Copyright 2012 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
from __future__ import (
9
"""Tests for `CobblerSession`."""
14
from random import Random
15
from xmlrpclib import Fault
16
from twisted.internet.defer import inlineCallbacks, returnValue
18
from testtools.deferredruntest import AsynchronousDeferredRunTest
19
from unittest import TestCase
20
from provisioningserver import cobblerclient
21
from provisioningserver.fakecobbler import fake_token
28
"""Pick an arbitrary number."""
29
return randomizer.randint(0, 10**9)
32
class FakeAuthFailure(Fault):
33
"""Imitated Cobbler authentication failure."""
35
def __init__(self, token):
36
super(FakeAuthFailure, self).__init__(1, "invalid token: %s" % token)
39
def make_auth_failure(broken_token=None):
40
"""Mimick a Cobbler authentication failure."""
41
if broken_token is None:
42
broken_token = fake_token()
43
return FakeAuthFailure(broken_token)
46
class InstrumentedSession(cobblerclient.CobblerSession):
47
"""Instrumented `CobblerSession` with a fake XMLRPC proxy.
49
:ivar fake_proxy: Test double for the XMLRPC proxy.
50
:ivar fake_token: Auth token that login will pretend to receive.
53
def __init__(self, *args, **kwargs):
54
"""Create and instrument a session.
56
In addition to the arguments for `CobblerSession.__init__`, pass a
57
keyword argument `fake_proxy` to set a test double that the session
58
will use for its proxy; and `fake_token` to provide a login token
59
that the session should pretend it gets from the server on login.
61
self.fake_proxy = kwargs['fake_proxy']
62
self.fake_token = kwargs['fake_token']
63
del kwargs['fake_proxy']
64
del kwargs['fake_token']
65
super(InstrumentedSession, self).__init__(*args, **kwargs)
67
def _make_twisted_proxy(self):
68
return self.fake_proxy
71
self.token = self.fake_token
74
class RecordingFakeProxy:
75
"""Simple fake Twisted XMLRPC proxy.
77
Records XMLRPC calls, and returns predetermined values.
81
self.return_values = None
83
def set_return_values(self, values):
84
"""Set predetermined value to return on following call(s).
86
If any return value is an `Exception`, it will be raised instead.
88
self.return_values = values
91
def callRemote(self, method, *args):
92
self.calls.append((method, ) + tuple(args))
93
if self.return_values:
94
value = self.return_values.pop(0)
97
if isinstance(value, Exception):
104
class TestCobblerSession(TestCase):
105
"""Test session management against a fake XMLRPC session."""
107
run_tests_with = AsynchronousDeferredRunTest.make_factory()
109
def make_url_user_password(self):
110
"""Produce arbitrary API URL, username, and password."""
112
'http://api.example.com/%d' % pick_number(),
113
'username%d' % pick_number(),
114
'password%d' % pick_number(),
117
def make_recording_session(self, session_args=None, token=None):
118
"""Create an `InstrumentedSession` with a `RecordingFakeProxy`."""
119
if session_args is None:
120
session_args = self.make_url_user_password()
123
fake_proxy = RecordingFakeProxy()
124
return InstrumentedSession(
125
*session_args, fake_proxy=fake_proxy, fake_token=token)
127
def test_initializes_but_does_not_authenticate_on_creation(self):
128
url, user, password = self.make_url_user_password()
129
session = self.make_recording_session(token=fake_token())
130
self.assertEqual(None, session.token)
132
def test_authenticate_authenticates_initially(self):
134
session = self.make_recording_session(token=token)
135
self.assertEqual(None, session.token)
136
session.authenticate()
137
self.assertEqual(token, session.token)
139
def test_state_cookie_stays_constant_during_normal_use(self):
140
session = self.make_recording_session()
141
state = session.record_state()
142
self.assertEqual(state, session.record_state())
143
session.call("some_method")
144
self.assertEqual(state, session.record_state())
146
def test_authentication_changes_state_cookie(self):
147
session = self.make_recording_session()
148
old_cookie = session.record_state()
149
session.authenticate()
150
self.assertNotEqual(old_cookie, session.record_state())
152
def test_authenticate_backs_off_from_overwriting_concurrent_auth(self):
153
session = self.make_recording_session()
154
# Two requests are made concurrently.
155
cookie_before_request_1 = session.record_state()
156
cookie_before_request_2 = session.record_state()
157
# Request 1 comes back with an authentication failure, and its
158
# callback refreshes the session's auth token.
159
session.authenticate(cookie_before_request_1)
160
token_for_retrying_request_1 = session.token
161
# Request 2 also comes back an authentication failure, and its
162
# callback also asks the session to ensure that it is
164
session.authenticate(cookie_before_request_2)
165
token_for_retrying_request_2 = session.token
167
# The double authentication does not confuse the session; both
168
# callbacks get the same auth token for their retries.
170
token_for_retrying_request_1, token_for_retrying_request_2)
171
# The token they get is a new token, not the one they started
173
self.assertNotEqual(cookie_before_request_1, session.token)
174
self.assertNotEqual(cookie_before_request_2, session.token)
176
def test_substitute_token_substitutes_only_placeholder(self):
178
session = self.make_recording_session(token=token)
179
session.authenticate()
180
arbitrary_number = pick_number()
183
cobblerclient.CobblerSession.token_placeholder,
190
self.assertEqual(outputs, map(session.substitute_token, inputs))
193
def test_call_calls_xmlrpc(self):
194
session = self.make_recording_session()
195
return_value = pick_number()
196
method = 'method%d' % pick_number()
198
session.fake_proxy.set_return_values([return_value])
199
actual_return_value = yield session.call(method, arg)
200
self.assertEqual(return_value, actual_return_value)
201
self.assertEqual([(method, arg)], session.fake_proxy.calls)
204
def test_call_reauthenticates_and_retries_on_auth_failure(self):
205
session = self.make_recording_session()
206
fake_proxy = session.fake_proxy
207
fake_proxy.set_return_values([make_auth_failure()])
208
fake_proxy.calls = []
209
yield session.call("failing_method")
212
# Initial call to failing_method: auth failure.
213
('failing_method', ),
214
# But call() re-authenticates, and retries.
215
('failing_method', ),
220
def test_call_raises_repeated_auth_failure(self):
221
session = self.make_recording_session()
222
session.fake_proxy.set_return_values([
223
# Initial operation fails: not authenticated.
225
# But retry still raises authentication failure.
229
d = session.call('double_fail')
230
return_value = yield d
234
self.fail("Returned %s instead of raising." % return_value)
237
def test_call_raises_general_failure(self):
238
session = self.make_recording_session()
239
session.fake_proxy.set_return_values([
240
Exception("Memory error. Where did I put it?"),
243
d = session.call('failing_method')
244
return_value = yield d
249
False, "Returned %s instead of raising." % return_value)