Merge lp:~benji/landscape-client/better-self-signed-cert-ux-3 into lp:~landscape/landscape-client/trunk

Proposed by Benji York
Status: Merged
Approved by: Benji York
Approved revision: 831
Merged at revision: 812
Proposed branch: lp:~benji/landscape-client/better-self-signed-cert-ux-3
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 1345 lines (+604/-570)
4 files modified
landscape/broker/amp.py (+15/-0)
landscape/configuration.py (+108/-90)
landscape/tests/helpers.py (+1/-0)
landscape/tests/test_configuration.py (+480/-480)
To merge this branch: bzr merge lp:~benji/landscape-client/better-self-signed-cert-ux-3
Reviewer Review Type Date Requested Status
Björn Tillenius (community) Approve
Chris Glass (community) Abstain
Данило Шеган (community) Approve
Review via email: mp+250841@code.launchpad.net

Commit message

This branch is a pure refactoring of the way the client configuration
communicates so as to separate the UI and network code in preparation
for better SSL error handling and user interaction.

Tests were then able to be improved -- but not as far as we would like.

Notes from Chris:

Most of the code breaks out nested function declaration to make them
easier to test, using functools.partial to keep the invocation sane in a
twisted context (test them "flat", use functools to pass closures to
event handlers).

We used manual stubs instead of mocks for readability and personal
preference [of both of us].

Description of the change

This branch is a pure refactoring of the way the client configuration communicates so as to separate the UI and network code in preparation for better SSL error handling and user interaction.

Tests were then able to be improved -- but not as far as we would like.

Notes from Chris:

Most of the code breaks out nested function declaration to make them easier to test, using functools.partial to keep the invocation sane in a twisted context (test them "flat", use functools to pass closures to event handlers).

We used manual stubs instead of mocks for readability and personal preference (of both of us).

To post a comment you must log in.
Revision history for this message
Chris Glass (tribaal) wrote :

Looks good, and seems to work.

The merge description is wrong however - this branch "only" refactors the current code to make it easier to grok and test, but there is currently no functional improvement (that should come in a subsequent branch).

Note (can be used in the change description):
Most of the code breaks out nested function declaration to make them easier to test, using functools.partial to keep the invocation sane in a twisted context (test them "flat", use functools to pass closures to event handlers).

Note 2:
We used manual stubs instead of mocks for readability and personal preference (of both of us).

I'll comment again when I will have performed actual manual tests (give me a couple of hours :) )

Revision history for this message
Benji York (benji) wrote :

> Looks good, and seems to work.
>
> The merge description is wrong however

I've fixed the MP description and incorporated your notes below.

> I'll comment again when I will have performed actual manual tests (give me a
> couple of hours :) )

/me crosses fingers.

Revision history for this message
Chris Glass (tribaal) wrote :

Summary testing succeeded.

+1

review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :

Seems to work fine against my self-signed Landscape. A few comments inline.

I am not too happy with the tests, since you seem to be mostly testing the wiring, and never that it actually works well together. I do understand that coming up with proper tests would require even more work, and with the understanding that this does not make the code base any worse, I am approving it.

Finally, I am not sure it's such a great idea to have Chris do one of the reviews _if_ he has contributed a lot of code/opinions while you were developing this branch. But you guys know best how much his mind is already "tainted" with the approach :)

review: Approve
Revision history for this message
Chris Glass (tribaal) wrote :

I will abstain from this one since I peer-programmed some of it.

review: Abstain
Revision history for this message
Chris Glass (tribaal) wrote :

Good point Danilo, I added another review slot and abstained.

Revision history for this message
Björn Tillenius (bjornt) wrote :

Why couldn't this be broken up into smaller branches?

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

> Why couldn't this be broken up into smaller branches?

Agreed. I didn't look at the code in detail, but I was expecting to see at least 2 branches: one for the pure refactoring of the registration code and one for adding the new functionality.

Revision history for this message
Chris Glass (tribaal) wrote :

Free, this *is* the pure refactoring branch.

Revision history for this message
Björn Tillenius (bjornt) wrote :

> Free, this *is* the pure refactoring branch.

In that case, the MP description is wrong. It says that more info is provided to the user and more error conditions are handled.

Revision history for this message
Benji York (benji) wrote :

> > Free, this *is* the pure refactoring branch.
>
> In that case, the MP description is wrong. It says that more info is provided
> to the user and more error conditions are handled.

I had some sort of edit failure and didn't get the MP description corrected. It is right now.

Revision history for this message
Benji York (benji) wrote :

> Why couldn't this be broken up into smaller branches?

It surely could have been, but given the nigh untestable nature of the current code structure (even after our refactoring), one large branch was the least risky way to proceed without undue exploratory testing, especially given Chris' upcoming disconnectedness.

Revision history for this message
Benji York (benji) wrote :

> Seems to work fine against my self-signed Landscape. A few comments inline.
>
> I am not too happy with the tests

As are we. Unfortunately, better tests would have required much more refactoring as well as having some place to put integration tests, which were expressly forbid when the idea of adding some to the existing test base was floated.

> Finally, I am not sure it's such a great idea to have Chris do one of the
> reviews _if_ he has contributed a lot of code/opinions while you were
> developing this branch.

Christ has recused himself from the review.

(I am of the opinion that his review would have been fine, given that I am not big on two-person reviews in the first place, and more so if a branch is the result of pair programming -- which this was.)

Revision history for this message
Данило Шеган (danilo) wrote :

Yes, agreed re better tests: I don't think it's worth it.

I am happy about the Christ not having an opinion on this branch :)

As for Chris, pair programming usually does lead to better code, but at the same time can result in two minds agreeing to decisions and never re-questioning them after the original agreement (I've been guilty of the same thing).

Anyway, my overall assesement is that this is improvement over the existing code (and tests are no worse) which is reflected in my approval :)

Revision history for this message
Benji York (benji) wrote :

Thanks for the good review Danilo. I fixed the things you pointed out inline. I also noticed some of the pre-existing tests were generating output to stdout, so I corrected those.

Revision history for this message
Björn Tillenius (bjornt) wrote :

Sorry, I won't have time to finish this review before my EOD. I added some initial comments inline.

Revision history for this message
Björn Tillenius (bjornt) wrote :

Ok, I have had time to look at this branch more closely now. How would you feel if I put it in Needs Fixing and requested you to rewrite the tests?

I understand that it's some work doing that, and that fakes are a small improvement over mocking, but the emphasis is on small here. The way you use fakes makes it very similar to mocking. It's still easier to read, but you're still testing the that the code is written in the same way the tests expects them to be.

I would rather that the tests were a bit more functional, in that you set up the conditions, then call register() and check what it returns. I don't care how the deferreds are set up internally. What I care about is that register() returns "success" when the registration succeeds.

It's not quite trivial, but here's an example of a test that checks that register() returns "success" when the registration is successful:

  https://pastebin.canonical.com/126660/

What do you think?

review: Needs Information
Revision history for this message
Benji York (benji) wrote :

> What do you think?

I haven't taken the time to look deeply into your proposed test approach, but I agree with everything you said. Unfortunately, the amount of time invested in this branch thus far means that the size of the marginal improvement must be very large for it to be worth it and regrettably, that's not the case.

I plan on keeping the test approach in mind for the next time we work on this code (which I hope is soon, it needs quite a bit of help).

Thank you for the review.

Revision history for this message
Björn Tillenius (bjornt) wrote :

Ok, I won't push too hard on adding better tests, since the existing tests weren't great either. But the old tests did actually tests that the async machinery really works, which your tests don't. So I would like you to at least add the test that I pasted, so you have one test that does end-to-end testing.

I'd also like you to respond to the inline comments I added here, as well as those I added previously, and push the latest revision please. I still don't see the changes you did in response to Danilo's comments.

Revision history for this message
Benji York (benji) :
827. By Benji York

docstring all the things

828. By Benji York

remove duplicate and redundant assertion

829. By Benji York

- add tests for reporting registration result to the user
- fix up tests that print to stdout so they won't

Revision history for this message
Benji York (benji) wrote :

> [...] So I would like you to at
> least add the test that I pasted, so you have one test that does end-to-end
> testing.

Done. (BTW, that diff was odd. I'm curious how you generated it. I had to hand apply it.)

> I'd also like you to respond to the inline comments I added here, as well as
> those I added previously

Done.

> and push the latest revision please.

Done.

Revision history for this message
Björn Tillenius (bjornt) wrote :

Sorry for being a pain :) But the latest diff isn't what I would expect it to be. It lacks at least one change that you said you would do (I added an inline comment), and the test I added isn't there. Instead you added a bunch of other tests that do mocking and seem redundant?

BTW, maybe the patch was odd because you looked at the wrong pastebin? ;) Although, the patch could look odd since I based it on what you pushed. Actually, looking at what you pushed, it's quite clear that you didn't push the changes you did in reply to my comments. You pushed revisions from Feb 26.

Please make it a habit to always push the latest code to LP. It's a bit complicated to review the branch when you know that there are other changes that you can't see.

830. By Benji York

- add a test that runs register() in a more full-boddied way
- use Twisted parameter passing functionality instead of partials
  - tweak parameter order to account for different calling style
- add and improve motivational comments around test assertions
- fix a test assertion that was unreachable

Revision history for this message
Benji York (benji) wrote :

> Sorry for being a pain :) But the latest diff isn't what I would expect it to
> be.

I pushed, but apparently forgot to commit beforehand.

> BTW, maybe the patch was odd because you looked at the wrong pastebin? ;)

This is the pastebin I was referring to: https://pastebin.canonical.com/126660/

The oddities I see are the blank lines with a dot in the indicator position and the lack of whitespace after the file name in the headers.

Revision history for this message
Björn Tillenius (bjornt) wrote :

Ok, let's get this branch landed now!

> This is the pastebin I was referring to: https://pastebin.canonical.com/126660/
>
> The oddities I see are the blank lines with a dot in the indicator position and the lack of
> whitespace after the file name in the headers.

Oh, that is odd indeed. I've never seen that before, but it seems to be since I copied
it from 'bzr diff | vim -' and the diff highlighter adds those periods.

review: Approve
831. By Benji York

merge from trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'landscape/broker/amp.py'
2--- landscape/broker/amp.py 2015-01-12 12:10:05 +0000
3+++ landscape/broker/amp.py 2015-03-04 14:53:14 +0000
4@@ -65,6 +65,21 @@
5 return maybeDeferred(callable, *args)
6 return succeed(None)
7
8+ def call_on_event(self, handlers):
9+ """Call a given handler as soon as a certain event occurs.
10+
11+ @param handlers: A dictionary mapping event types to callables, where
12+ an event type is string (the name of the event). When the first of
13+ the given event types occurs in the broker reactor, the associated
14+ callable will be fired.
15+ """
16+ result = self.broker_server.listen_events(handlers.keys())
17+ return result.addCallback(
18+ lambda (event_type, kwargs): handlers[event_type](**kwargs))
19+
20+ def register(self):
21+ return succeed(None)
22+
23
24 class RemoteBrokerConnector(ComponentConnector):
25 """Helper to create connections with the L{BrokerServer}."""
26
27=== modified file 'landscape/configuration.py'
28--- landscape/configuration.py 2015-01-12 09:27:02 +0000
29+++ landscape/configuration.py 2015-03-04 14:53:14 +0000
30@@ -4,12 +4,14 @@
31 for the C{landscape-config} script.
32 """
33
34+from __future__ import print_function
35+
36+from functools import partial
37 import base64
38-import time
39-import sys
40+import getpass
41 import os
42-import getpass
43 import pwd
44+import sys
45
46 from StringIO import StringIO
47
48@@ -35,6 +37,8 @@
49
50
51 def print_text(text, end="\n", error=False):
52+ """Display the given text to the user, using stderr if flagged as an error.
53+ """
54 if error:
55 stream = sys.stderr
56 else:
57@@ -584,8 +588,60 @@
58 return key_filename
59
60
61-def register(config, on_message=print_text, on_error=sys.exit, reactor=None,
62- max_retries=14):
63+def failure(add_result):
64+ """Handle a failed communication by recording the kind of failure."""
65+ add_result("failure")
66+
67+
68+def exchange_failure(add_result, ssl_error=False):
69+ """Handle a failed call by recording if the failure was SSL-related."""
70+ if ssl_error:
71+ add_result("ssl-error")
72+ else:
73+ add_result("non-ssl-error")
74+
75+
76+def handle_registration_errors(add_result, connector, failure):
77+ """HAndle a failed registration by recording the kind of failure."""
78+ # We'll get invalid credentials through the signal.
79+ failure.trap(InvalidCredentialsError, MethodCallError)
80+ add_result("registration-error")
81+ connector.disconnect()
82+
83+
84+def success(add_result):
85+ """Handle a successful communication by recording the fact."""
86+ add_result("success")
87+
88+
89+def done(ignored_result, connector, reactor):
90+ """Clean up after communicating with the server."""
91+ connector.disconnect()
92+ reactor.stop()
93+
94+
95+def got_connection(add_result, connector, reactor, remote):
96+ """Handle becomming connected to a broker."""
97+ handlers = {"registration-done": partial(success, add_result),
98+ "registration-failed": partial(failure, add_result),
99+ "exchange-failed": partial(exchange_failure, add_result)}
100+ deferreds = [
101+ remote.call_on_event(handlers),
102+ remote.register().addErrback(
103+ handle_registration_errors, add_result, connector)]
104+ results = gather_results(deferreds)
105+ results.addCallback(done, connector, reactor)
106+ return results
107+
108+
109+def got_error(failure, print=print):
110+ """...from broker."""
111+ print(failure.getTraceback(), file=sys.stderr)
112+ raise SystemExit
113+
114+
115+def register(config, reactor, connector_factory=RemoteBrokerConnector,
116+ got_connection=got_connection, max_retries=14, results=None):
117 """Instruct the Landscape Broker to register the client.
118
119 The broker will be instructed to reload its configuration and then to
120@@ -605,91 +661,48 @@
121 max_retries = 14
122
123 0.05 * (1 - 1.62 ** 14) / (1 - 1.62) = 69 seconds
124- """
125- if reactor is None:
126- reactor = LandscapeReactor()
127- exit_with_error = []
128-
129- def stop(errors):
130- if not config.ok_no_register:
131- for error in errors:
132- if error is not None:
133- exit_with_error.append(error)
134- connector.disconnect()
135- reactor.stop()
136-
137- def failure():
138- on_message("Invalid account name or "
139- "registration key.", error=True)
140- return 2
141-
142- def success():
143- on_message("System successfully registered.")
144-
145- def exchange_failure(ssl_error=False):
146- if ssl_error:
147- message = ("\nThe server's SSL information is incorrect, or fails "
148- "signature verification!\n"
149- "If the server is using a self-signed certificate, "
150- "please ensure you supply it with the --ssl-public-key "
151- "parameter.")
152- else:
153- message = ("\nWe were unable to contact the server.\n"
154- "Your internet connection may be down. "
155- "The landscape client will continue to try and contact "
156- "the server periodically.")
157-
158- on_message(message, error=True)
159- return 2
160-
161- def handle_registration_errors(failure):
162- # We'll get invalid credentials through the signal.
163- failure.trap(InvalidCredentialsError, MethodCallError)
164- connector.disconnect()
165-
166- def catch_all(failure):
167- on_message(failure.getTraceback(), error=True)
168- on_message("Unknown error occurred.", error=True)
169- return [2]
170-
171- on_message("Please wait... ", "")
172-
173- time.sleep(2)
174-
175- def got_connection(remote):
176- handlers = {"registration-done": success,
177- "registration-failed": failure,
178- "exchange-failed": exchange_failure}
179- deferreds = [
180- remote.call_on_event(handlers),
181- remote.register().addErrback(handle_registration_errors)]
182- # We consume errors here to ignore errors after the first one.
183- # catch_all will be called for the very first deferred that fails.
184- results = gather_results(deferreds, consume_errors=True)
185- results.addErrback(catch_all)
186- results.addCallback(stop)
187-
188- def got_error(failure):
189- on_message("There was an error communicating with the Landscape"
190- " client.", error=True)
191- on_message("This machine will be registered with the provided "
192- "details when the client runs.", error=True)
193- stop([2])
194-
195- connector = RemoteBrokerConnector(reactor, config)
196- result = connector.connect(max_retries=max_retries, quiet=True)
197- result.addCallback(got_connection)
198- result.addErrback(got_error)
199-
200+ """
201+ if results is None:
202+ results = []
203+ add_result = results.append
204+
205+ connector = connector_factory(reactor, config)
206+ connection = connector.connect(max_retries=max_retries, quiet=True)
207+ connection.addCallback(
208+ partial(got_connection, add_result, connector, reactor))
209+ connection.addErrback(got_error)
210 reactor.run()
211
212- if exit_with_error:
213- on_error(exit_with_error[0])
214-
215- return result
216-
217-
218-def main(args):
219+ assert len(results) == 1, "We expect exactly one result."
220+ # Results will be things like "success" or "ssl-error".
221+ return results[0]
222+
223+
224+def report_registration_outcome(what_happened, print=print):
225+ """
226+ Report the registrtion interaction outcome to the user in human-readable
227+ form.
228+ """
229+ if what_happened == "success":
230+ print("System successfully registered.")
231+ elif what_happened == "failure":
232+ print("Invalid account name or registration key.", file=sys.stderr)
233+ elif what_happened == "ssl-error":
234+ print("\nThe server's SSL information is incorrect, or fails "
235+ "signature verification!\n"
236+ "If the server is using a self-signed certificate, "
237+ "please ensure you supply it with the --ssl-public-key "
238+ "parameter.", file=sys.stderr)
239+ elif what_happened == "non-ssl-error":
240+ print("\nWe were unable to contact the server.\n"
241+ "Your internet connection may be down. "
242+ "The landscape client will continue to try and contact "
243+ "the server periodically.", file=sys.stderr)
244+
245+
246+def main(args, print=print):
247+ """Interact with the user and the server to set up client configuration."""
248+
249 config = LandscapeSetupConfiguration()
250 try:
251 config.load(args)
252@@ -716,11 +729,16 @@
253 print_text(str(e))
254 sys.exit("Aborting Landscape configuration")
255
256+ print("Please wait...")
257+
258 # Attempt to register the client.
259+ reactor = LandscapeReactor()
260 if config.silent:
261- register(config)
262+ result = register(config, reactor)
263+ report_registration_outcome(result, print=print)
264 else:
265 answer = raw_input("\nRequest a new registration for "
266 "this computer now? (Y/n): ")
267 if not answer.upper().startswith("N"):
268- register(config)
269+ result = register(config, reactor)
270+ report_registration_outcome(result, print=print)
271
272=== modified file 'landscape/tests/helpers.py'
273--- landscape/tests/helpers.py 2013-09-25 08:43:17 +0000
274+++ landscape/tests/helpers.py 2015-03-04 14:53:14 +0000
275@@ -359,6 +359,7 @@
276 transport_factory = FakeTransport
277
278 test_case.broker_service = FakeBrokerService(config)
279+ test_case.reactor = test_case.broker_service.reactor
280 test_case.remote = FakeRemoteBroker(
281 test_case.broker_service.exchanger,
282 test_case.broker_service.message_store,
283
284=== modified file 'landscape/tests/test_configuration.py'
285--- landscape/tests/test_configuration.py 2015-01-12 09:29:17 +0000
286+++ landscape/tests/test_configuration.py 2015-03-04 14:53:14 +0000
287@@ -1,27 +1,29 @@
288+from __future__ import print_function
289+
290+from ConfigParser import ConfigParser
291+from cStringIO import StringIO
292+from getpass import getpass
293 import os
294 import sys
295-from getpass import getpass
296-from ConfigParser import ConfigParser
297-from cStringIO import StringIO
298-
299-from twisted.internet.defer import succeed, fail
300-from twisted.internet.task import Clock
301-
302-from landscape.lib.amp import MethodCallSender
303-from landscape.reactor import LandscapeReactor, FakeReactor
304+import unittest
305+
306+from twisted.internet.defer import succeed
307+
308+from landscape.broker.registration import InvalidCredentialsError
309+from landscape.tests.helpers import FakeBrokerServiceHelper
310+from landscape.broker.tests.helpers import RemoteBrokerHelper
311+from landscape.lib.amp import MethodCallError
312 from landscape.lib.fetch import HTTPCodeError, PyCurlError
313 from landscape.configuration import (
314 print_text, LandscapeSetupScript, LandscapeSetupConfiguration,
315 register, setup, main, setup_init_script_and_start_client,
316 stop_client_and_disable_init_script, ConfigurationError,
317 ImportOptionError, store_public_key_data,
318- bootstrap_tree)
319-from landscape.broker.registration import InvalidCredentialsError
320+ bootstrap_tree, got_connection, success, failure, exchange_failure,
321+ handle_registration_errors, done, got_error, report_registration_outcome)
322 from landscape.sysvconfig import SysVConfig, ProcessError
323-from landscape.tests.helpers import (
324- LandscapeTest, BrokerServiceHelper, EnvironSaverHelper)
325-from landscape.tests.mocker import ARGS, ANY, MATCH, CONTAINS, expect
326-from landscape.broker.amp import RemoteBroker
327+from landscape.tests.helpers import LandscapeTest, EnvironSaverHelper
328+from landscape.tests.mocker import ANY, MATCH, CONTAINS, expect
329
330
331 class LandscapeConfigurationTest(LandscapeTest):
332@@ -41,6 +43,113 @@
333 return config
334
335
336+class SuccessTests(unittest.TestCase):
337+ def test_success(self):
338+ """The success handler records the success."""
339+ results = []
340+ success(results.append)
341+ self.assertEqual(["success"], results)
342+
343+
344+class FailureTests(unittest.TestCase):
345+ def test_failure(self):
346+ """The failure handler records the failure and returns non-zero."""
347+ results = []
348+ self.assertNotEqual(0, failure(results.append))
349+ self.assertEqual(["failure"], results)
350+
351+
352+class ExchangeFailureTests(unittest.TestCase):
353+
354+ def test_exchange_failure_ssl(self):
355+ """The exchange_failure() handler records whether or not the failure
356+ involved SSL or not and returns non-zero."""
357+ results = []
358+ self.assertNotEqual(0,
359+ exchange_failure(results.append, ssl_error=True))
360+ self.assertEqual(["ssl-error"], results)
361+
362+ def test_exchange_failure_non_ssl(self):
363+ """
364+ The exchange_failure() handler records whether or not the failure
365+ involved SSL or not and returns non-zero.
366+ """
367+ results = []
368+ self.assertNotEqual(0,
369+ exchange_failure(results.append, ssl_error=False))
370+ self.assertEqual(["non-ssl-error"], results)
371+
372+
373+class HandleRegistrationErrorsTests(unittest.TestCase):
374+
375+ def test_handle_registration_errors(self):
376+ """
377+ The handle_registration_errors() function handles
378+ InvalidCredentialsError and MethodCallError errors and records the
379+ type of failure and disconnects.
380+ """
381+ class FauxFailure(object):
382+ def trap(self, *trapped):
383+ self.trapped_exceptions = trapped
384+
385+ faux_connector = FauxConnector()
386+ faux_failure = FauxFailure()
387+
388+ results = []
389+ self.assertNotEqual(0,
390+ handle_registration_errors(
391+ results.append, faux_connector, faux_failure))
392+ self.assertEqual(["registration-error"], results)
393+ self.assertTrue(faux_connector.was_disconnected)
394+ self.assertTrue(
395+ [InvalidCredentialsError, MethodCallError],
396+ faux_failure.trapped_exceptions)
397+
398+
399+class DoneTests(unittest.TestCase):
400+
401+ def test_done(self):
402+ """The done() function handles cleaning up."""
403+ class FauxConnector(object):
404+ was_disconnected = False
405+
406+ def disconnect(self):
407+ self.was_disconnected = True
408+
409+ class FauxReactor(object):
410+ was_stopped = False
411+
412+ def stop(self):
413+ self.was_stopped = True
414+
415+ faux_connector = FauxConnector()
416+ faux_reactor = FauxReactor()
417+
418+ done(None, faux_connector, faux_reactor)
419+ self.assertTrue(faux_connector.was_disconnected)
420+ self.assertTrue(faux_reactor.was_stopped)
421+
422+
423+class GotErrorTests(unittest.TestCase):
424+
425+ def test_got_error(self):
426+ """The got_error() function handles displaying errors and exiting."""
427+ class FauxFailure(object):
428+
429+ def getTraceback(self):
430+ return "traceback"
431+
432+ printed = []
433+
434+ def faux_print(text, file):
435+ printed.append((text, file))
436+
437+ with self.assertRaises(SystemExit):
438+ got_error(FauxFailure(), print=faux_print)
439+
440+ self.assertEqual([('traceback', sys.stderr)], printed)
441+
442+
443 class PrintTextTest(LandscapeTest):
444
445 def test_default(self):
446@@ -683,6 +792,11 @@
447 self.assertTrue(os.path.isdir(annotations_path))
448
449
450+def noop_print(*args, **kws):
451+ """A print that doesn't do anything."""
452+ pass
453+
454+
455 class ConfigurationFunctionsTest(LandscapeConfigurationTest):
456
457 helpers = [EnvironSaverHelper]
458@@ -1022,12 +1136,12 @@
459
460 # This must not be called.
461 register_mock = self.mocker.replace(register, passthrough=False)
462- register_mock(ANY)
463+ register_mock(ANY, ANY)
464 self.mocker.count(0)
465
466 self.mocker.replay()
467
468- main(["-c", self.make_working_config()])
469+ main(["-c", self.make_working_config()], print=noop_print)
470
471 def test_main_silent(self):
472 """
473@@ -1038,7 +1152,7 @@
474 setup_mock(ANY)
475
476 register_mock = self.mocker.replace(register, passthrough=False)
477- register_mock(ANY)
478+ register_mock(ANY, ANY)
479 self.mocker.count(1)
480
481 self.mocker.replay()
482@@ -1049,7 +1163,121 @@
483 "account_name = Old Name\n"
484 "registration_key = Old Password\n"
485 )
486- main(["-c", config_filename, "--silent"])
487+ main(["-c", config_filename, "--silent"], print=noop_print)
488+
489+ def test_main_user_interaction_success(self):
490+ """The successful result of register() is communicated to the user."""
491+ setup_mock = self.mocker.replace(setup)
492+ setup_mock(ANY)
493+
494+ raw_input_mock = self.mocker.replace(raw_input)
495+ raw_input_mock("\nRequest a new registration for "
496+ "this computer now? (Y/n): ")
497+ self.mocker.result("y")
498+
499+ # The register() function will be called.
500+ register_mock = self.mocker.replace(register, passthrough=False)
501+ register_mock(ANY, ANY)
502+ self.mocker.result("success")
503+
504+ self.mocker.replay()
505+
506+ printed = []
507+
508+ def faux_print(string, file=sys.stdout):
509+ printed.append((string, file))
510+
511+ main(["-c", self.make_working_config()], print=faux_print)
512+ self.assertEqual(
513+ [("Please wait...", sys.stdout),
514+ ("System successfully registered.", sys.stdout)],
515+ printed)
516+
517+ def test_main_user_interaction_failure(self):
518+ """The failed result of register() is communicated to the user."""
519+ setup_mock = self.mocker.replace(setup)
520+ setup_mock(ANY)
521+
522+ raw_input_mock = self.mocker.replace(raw_input)
523+ raw_input_mock("\nRequest a new registration for "
524+ "this computer now? (Y/n): ")
525+ self.mocker.result("y")
526+
527+ # The register() function will be called.
528+ register_mock = self.mocker.replace(register, passthrough=False)
529+ register_mock(ANY, ANY)
530+ self.mocker.result("failure")
531+
532+ self.mocker.replay()
533+
534+ printed = []
535+
536+ def faux_print(string, file=sys.stdout):
537+ printed.append((string, file))
538+
539+ main(["-c", self.make_working_config()], print=faux_print)
540+ # Note that the error is output via sys.stderr.
541+ self.assertEqual(
542+ [("Please wait...", sys.stdout),
543+ ("Invalid account name or registration key.", sys.stderr)],
544+ printed)
545+
546+ def test_main_user_interaction_success_silent(self):
547+ """A successful result is communicated to the user even with --silent.
548+ """
549+ setup_mock = self.mocker.replace(setup)
550+ setup_mock(ANY)
551+
552+ raw_input_mock = self.mocker.replace(raw_input)
553+ raw_input_mock(ANY)
554+ self.mocker.count(0)
555+
556+ # The register() function will be called.
557+ register_mock = self.mocker.replace(register, passthrough=False)
558+ register_mock(ANY, ANY)
559+ self.mocker.result("success")
560+
561+ self.mocker.replay()
562+
563+ printed = []
564+
565+ def faux_print(string, file=sys.stdout):
566+ printed.append((string, file))
567+
568+ main(["--silent", "-c", self.make_working_config()], print=faux_print)
569+ self.assertEqual(
570+ [("Please wait...", sys.stdout),
571+ ("System successfully registered.", sys.stdout)],
572+ printed)
573+
574+ def test_main_user_interaction_failure_silent(self):
575+ """A failure result is communicated to the user even with --silent.
576+ """
577+ setup_mock = self.mocker.replace(setup)
578+ setup_mock(ANY)
579+
580+ raw_input_mock = self.mocker.replace(raw_input)
581+ raw_input_mock(ANY)
582+ self.mocker.count(0)
583+
584+ # The register() function will be called.
585+ register_mock = self.mocker.replace(register, passthrough=False)
586+ register_mock(ANY, ANY)
587+ self.mocker.result("failure")
588+
589+ self.mocker.replay()
590+
591+ printed = []
592+
593+ def faux_print(string, file=sys.stdout):
594+ printed.append((string, file))
595+
596+ main(["--silent", "-c", self.make_working_config()], print=faux_print)
597+ # Note that the error is output via sys.stderr.
598+ self.assertEqual(
599+ [("Please wait...", sys.stdout),
600+ ("Invalid account name or registration key.", sys.stderr)],
601+ printed)
602
603 def make_working_config(self):
604 return self.makeFile("[client]\n"
605@@ -1081,10 +1309,10 @@
606 self.mocker.result("")
607
608 register_mock = self.mocker.replace(register, passthrough=False)
609- register_mock(ANY)
610+ register_mock(ANY, ANY)
611
612 self.mocker.replay()
613- main(["--config", self.make_working_config()])
614+ main(["--config", self.make_working_config()], print=noop_print)
615
616 def test_errors_from_restart_landscape(self):
617 """
618@@ -1141,10 +1369,10 @@
619 self.mocker.result("")
620
621 register_mock = self.mocker.replace(register, passthrough=False)
622- register_mock(ANY)
623+ register_mock(ANY, ANY)
624
625 self.mocker.replay()
626- main(["-c", self.make_working_config()])
627+ main(["-c", self.make_working_config()], print=noop_print)
628
629 def test_setup_init_script_and_start_client(self):
630 sysvconfig_mock = self.mocker.patch(SysVConfig)
631@@ -1178,11 +1406,11 @@
632 # The registration logic should be called and passed the configuration
633 # file.
634 register_mock = self.mocker.replace(register, passthrough=False)
635- register_mock(ANY)
636+ register_mock(ANY, ANY)
637
638 self.mocker.replay()
639
640- main(["--silent", "-c", self.make_working_config()])
641+ main(["--silent", "-c", self.make_working_config()], print=noop_print)
642
643 def test_disable(self):
644 stop_client_and_disable_init_script_mock = self.mocker.replace(
645@@ -1672,469 +1900,203 @@
646 # we care about are done.
647
648
649+class FakeConnectorFactory(object):
650+
651+ def __init__(self, remote):
652+ self.remote = remote
653+
654+ def __call__(self, reactor, config):
655+ self.reactor = reactor
656+ self.config = config
657+ return self
658+
659+ def connect(self, max_retries=None, quiet=False):
660+ return succeed(self.remote)
661+
662+ def disconnect(self):
663+ return succeed(None)
664+
665+
666+class RegisterRealFunctionTest(LandscapeConfigurationTest):
667+
668+ helpers = [FakeBrokerServiceHelper]
669+
670+ def setUp(self):
671+ super(RegisterRealFunctionTest, self).setUp()
672+ self.config = LandscapeSetupConfiguration()
673+ self.config.load(["-c", self.config_filename])
674+
675+ def test_register_success(self):
676+ self.reactor.call_later(0, self.reactor.fire, "registration-done")
677+ connector_factory = FakeConnectorFactory(self.remote)
678+ result = register(
679+ self.config, self.reactor, connector_factory, max_retries=99)
680+ self.assertEqual("success", result)
681+
682+
683+class FauxConnection(object):
684+ def __init__(self):
685+ self.callbacks = []
686+ self.errbacks = []
687+
688+ def addCallback(self, func, *args, **kws):
689+ self.callbacks.append(func)
690+
691+ def addErrback(self, func, *args, **kws):
692+ self.errbacks.append(func)
693+
694+
695+class FauxConnector(object):
696+
697+ was_disconnected = False
698+
699+ def __init__(self, reactor=None, config=None):
700+ self.reactor = reactor
701+ self.config = config
702+
703+ def connect(self, max_retries, quiet):
704+ self.max_retries = max_retries
705+ self.connection = FauxConnection()
706+ return self.connection
707+
708+ def disconnect(self):
709+ self.was_disconnected = True
710+
711+
712 class RegisterFunctionTest(LandscapeConfigurationTest):
713
714- helpers = [BrokerServiceHelper]
715+ helpers = [RemoteBrokerHelper]
716
717 def setUp(self):
718 super(RegisterFunctionTest, self).setUp()
719 self.config = LandscapeSetupConfiguration()
720 self.config.load(["-c", self.config_filename])
721
722- def test_register_success(self):
723- service = self.broker_service
724-
725- registration_mock = self.mocker.replace(service.registration)
726- print_text_mock = self.mocker.replace(print_text)
727- reactor_mock = self.mocker.patch(FakeReactor)
728-
729- # This must necessarily happen in the following order.
730- self.mocker.order()
731-
732- # This very informative message is printed out.
733- print_text_mock("Please wait... ", "")
734-
735- time_mock = self.mocker.replace("time")
736- time_mock.sleep(ANY)
737- self.mocker.count(1)
738-
739- # The register() method is called. We fire the "registration-done"
740- # event after it's done, so that it cascades into a deferred callback.
741-
742- def register_done():
743- service.reactor.fire("registration-done")
744-
745- registration_mock.register()
746- self.mocker.call(register_done)
747-
748- # The deferred callback finally prints out this message.
749- print_text_mock("System successfully registered.")
750-
751- reactor_mock.stop()
752-
753- # This is actually called after everything else since all deferreds
754- # are synchronous and callbacks will be executed immediately.
755- reactor_mock.run()
756-
757- # Nothing else is printed!
758- print_text_mock(ANY)
759- self.mocker.count(0)
760-
761- self.mocker.replay()
762-
763- # DO IT!
764- return register(self.config, print_text, sys.exit,
765- reactor=FakeReactor())
766-
767- def test_register_failure(self):
768- """
769- When registration fails because of invalid credentials, a message will
770- be printed to the console and the program will exit.
771- """
772- service = self.broker_service
773-
774- self.log_helper.ignore_errors(InvalidCredentialsError)
775- registration_mock = self.mocker.replace(service.registration)
776- print_text_mock = self.mocker.replace(print_text)
777- reactor_mock = self.mocker.patch(FakeReactor)
778-
779- # This must necessarily happen in the following order.
780- self.mocker.order()
781-
782- # This very informative message is printed out.
783- print_text_mock("Please wait... ", "")
784-
785- time_mock = self.mocker.replace("time")
786- time_mock.sleep(ANY)
787- self.mocker.count(1)
788-
789- # The register() method is called. We fire the "registration-failed"
790- # event after it's done, so that it cascades into a deferred errback.
791-
792- def register_done():
793- service.reactor.fire("registration-failed")
794-
795- registration_mock.register()
796- self.mocker.call(register_done)
797-
798- # The deferred errback finally prints out this message.
799- print_text_mock("Invalid account name or registration key.",
800- error=True)
801-
802- reactor_mock.stop()
803-
804- # This is actually called after everything else since all deferreds
805- # are synchronous and callbacks will be executed immediately.
806- reactor_mock.run()
807-
808- # Nothing else is printed!
809- print_text_mock(ANY)
810- self.mocker.count(0)
811-
812- self.mocker.replay()
813-
814- # DO IT!
815- exit = []
816- register(self.config, print_text, exit.append, reactor=FakeReactor())
817- self.assertEqual([2], exit)
818-
819- def test_register_exchange_failure(self):
820- """
821- When registration fails because the server couldn't be contacted, a
822- message is printed and the program quits.
823- """
824- service = self.broker_service
825-
826- registration_mock = self.mocker.replace(service.registration)
827- print_text_mock = self.mocker.replace(print_text)
828- reactor_mock = self.mocker.patch(FakeReactor)
829-
830- # This must necessarily happen in the following order.
831- self.mocker.order()
832-
833- # This very informative message is printed out.
834- print_text_mock("Please wait... ", "")
835-
836- time_mock = self.mocker.replace("time")
837- time_mock.sleep(ANY)
838- self.mocker.count(1)
839-
840- def register_done():
841- service.reactor.fire("exchange-failed")
842- registration_mock.register()
843- self.mocker.call(register_done)
844-
845- # The deferred errback finally prints out this message.
846- print_text_mock("\nWe were unable to contact the server.\n"
847- "Your internet connection may be down. "
848- "The landscape client will continue to try and "
849- "contact the server periodically.",
850- error=True)
851-
852- reactor_mock.stop()
853-
854- # This is actually called after everything else since all deferreds
855- # are synchronous and callbacks will be executed immediately.
856- reactor_mock.run()
857-
858- # Nothing else is printed!
859- print_text_mock(ANY)
860- self.mocker.count(0)
861-
862- self.mocker.replay()
863-
864- # DO IT!
865- exit = []
866- register(self.config, print_text, exit.append, reactor=FakeReactor())
867- self.assertEqual([2], exit)
868-
869- def test_register_exchange_SSL_failure(self):
870- """
871- When registration fails because the server's SSL certificate could not
872- be validated, a message is printed and the program quits.
873- """
874- service = self.broker_service
875-
876- registration_mock = self.mocker.replace(service.registration)
877- print_text_mock = self.mocker.replace(print_text)
878- reactor_mock = self.mocker.patch(FakeReactor)
879-
880- # This must necessarily happen in the following order.
881- self.mocker.order()
882-
883- # This very informative message is printed out.
884- print_text_mock("Please wait... ", "")
885-
886- time_mock = self.mocker.replace("time")
887- time_mock.sleep(ANY)
888- self.mocker.count(1)
889-
890- def register_done():
891- service.reactor.fire("exchange-failed", ssl_error=True)
892- registration_mock.register()
893- self.mocker.call(register_done)
894-
895- # The deferred errback finally prints out this message.
896- print_text_mock("\nThe server's SSL information is incorrect, or "
897- "fails signature verification!\n"
898- "If the server is using a self-signed certificate, "
899- "please ensure you supply it with the "
900- "--ssl-public-key parameter.",
901- error=True)
902-
903- reactor_mock.stop()
904-
905- # This is actually called after everything else since all deferreds
906- # are synchronous and callbacks will be executed immediately.
907- reactor_mock.run()
908-
909- # Nothing else is printed!
910- print_text_mock(ANY)
911- self.mocker.count(0)
912-
913- self.mocker.replay()
914-
915- # DO IT!
916- exit = []
917- register(self.config, print_text, exit.append, reactor=FakeReactor())
918- self.assertEqual([2], exit)
919-
920- def test_register_timeout_failure(self):
921- service = self.broker_service
922-
923- registration_mock = self.mocker.replace(service.registration)
924- print_text_mock = self.mocker.replace(print_text)
925- reactor_mock = self.mocker.patch(FakeReactor)
926- remote_mock = self.mocker.patch(RemoteBroker)
927-
928- protocol_mock = self.mocker.patch(MethodCallSender)
929- protocol_mock.timeout
930- self.mocker.result(0.1)
931- self.mocker.count(0, None)
932-
933- # This must necessarily happen in the following order.
934- self.mocker.order()
935-
936- # This very informative message is printed out.
937- print_text_mock("Please wait... ", "")
938-
939- time_mock = self.mocker.replace("time")
940- time_mock.sleep(ANY)
941- self.mocker.count(1)
942-
943- remote_mock.call_on_event(ANY)
944- self.mocker.result(succeed(None))
945-
946- registration_mock.register()
947- self.mocker.passthrough()
948-
949- # This is actually called after everything else since all deferreds
950- # are synchronous and callbacks will be executed immediately.
951- reactor_mock.run()
952-
953- # Nothing else is printed!
954- print_text_mock(ANY)
955- self.mocker.count(0)
956-
957- self.mocker.replay()
958-
959- # DO IT!
960- fake_reactor = FakeReactor()
961- fake_reactor._reactor = Clock()
962- deferred = register(self.config, print_text, sys.exit,
963- reactor=fake_reactor)
964- fake_reactor._reactor.advance(100)
965- return deferred
966-
967- def test_register_bus_connection_failure(self):
968- """
969- If the socket can't be connected to, landscape-config will print an
970- explanatory message and exit cleanly.
971- """
972- # This will make the RemoteBrokerConnector.connect call fail
973- print_text_mock = self.mocker.replace(print_text)
974- time_mock = self.mocker.replace("time")
975- sys_mock = self.mocker.replace("sys")
976- reactor_mock = self.mocker.patch(LandscapeReactor)
977-
978- connector_factory = self.mocker.replace(
979- "landscape.broker.amp.RemoteBrokerConnector", passthrough=False)
980- connector = connector_factory(ANY, ANY)
981- connector.connect(max_retries=0, quiet=True)
982- self.mocker.result(fail(ZeroDivisionError))
983-
984- print_text_mock(ARGS)
985- time_mock.sleep(ANY)
986- reactor_mock.run()
987-
988- print_text_mock(
989- CONTAINS("There was an error communicating with the "
990- "Landscape client"),
991- error=True)
992- print_text_mock(CONTAINS("This machine will be registered"),
993- error=True)
994-
995- sys_mock.exit(2)
996- connector.disconnect()
997- reactor_mock.stop()
998-
999- self.mocker.replay()
1000-
1001- config = self.get_config(["-a", "accountname", "--silent"])
1002- return register(config, print_text, sys.exit, max_retries=0)
1003-
1004- def test_register_bus_connection_failure_ok_no_register(self):
1005- """
1006- Exit code 0 will be returned if we can't contact Landscape via DBus and
1007- --ok-no-register was passed.
1008- """
1009- print_text_mock = self.mocker.replace(print_text)
1010- time_mock = self.mocker.replace("time")
1011- reactor_mock = self.mocker.patch(LandscapeReactor)
1012-
1013- print_text_mock(ARGS)
1014- time_mock.sleep(ANY)
1015- reactor_mock.run()
1016- reactor_mock.stop()
1017-
1018- print_text_mock(
1019- CONTAINS("There was an error communicating with the "
1020- "Landscape client"),
1021- error=True)
1022- print_text_mock(CONTAINS("This machine will be registered"),
1023- error=True)
1024-
1025- self.mocker.replay()
1026-
1027- config = self.get_config(
1028- ["-a", "accountname", "--silent", "--ok-no-register"])
1029- return self.assertSuccess(register(config, print_text, sys.exit,
1030- max_retries=0))
1031-
1032-
1033-class RegisterFunctionRetryTest(LandscapeConfigurationTest):
1034-
1035- helpers = [BrokerServiceHelper]
1036-
1037- def setUp(self):
1038- super(RegisterFunctionRetryTest, self).setUp()
1039- self.config = LandscapeSetupConfiguration()
1040- self.config.load(["-c", self.config_filename])
1041-
1042- def test_register_with_retry_parameters(self):
1043- """
1044- Retry parameters are passed to the L{connect} method of the connector.
1045- """
1046- print_text_mock = self.mocker.replace(print_text)
1047- time_mock = self.mocker.replace("time")
1048- sys_mock = self.mocker.replace("sys")
1049- reactor_mock = self.mocker.patch(LandscapeReactor)
1050-
1051- connector_factory = self.mocker.replace(
1052- "landscape.broker.amp.RemoteBrokerConnector", passthrough=False)
1053- connector = connector_factory(ANY, ANY)
1054- connector.connect(quiet=True, max_retries=12)
1055-
1056- self.mocker.result(succeed(None))
1057-
1058- print_text_mock(ARGS)
1059- time_mock.sleep(ANY)
1060- reactor_mock.run()
1061-
1062- print_text_mock(
1063- CONTAINS("There was an error communicating with the "
1064- "Landscape client"),
1065- error=True)
1066- print_text_mock(CONTAINS("This machine will be registered"),
1067- error=True)
1068-
1069- sys_mock.exit(2)
1070- connector.disconnect()
1071- reactor_mock.stop()
1072-
1073- self.mocker.replay()
1074-
1075- config = self.get_config(["-a", "accountname", "--silent"])
1076- return register(config, print_text, sys.exit, max_retries=12)
1077-
1078- def test_register_with_default_retry_parameters(self):
1079- """
1080- max_retries has reasonable default behavior - retry 14 times which
1081- will result in a wait of about 60 seconds, until the broker has time
1082- to start on heavily loaded systems.
1083-
1084- initialDelay = 0.05
1085- factor = 1.62
1086- maxDelay = 30
1087- max_retries = 14
1088-
1089- 0.05 * (1 - 1.62 ** 14) / (1 - 1.62) = 69 seconds
1090- """
1091- print_text_mock = self.mocker.replace(print_text)
1092- time_mock = self.mocker.replace("time")
1093- sys_mock = self.mocker.replace("sys")
1094- reactor_mock = self.mocker.patch(LandscapeReactor)
1095-
1096- connector_factory = self.mocker.replace(
1097- "landscape.broker.amp.RemoteBrokerConnector", passthrough=False)
1098- connector = connector_factory(ANY, ANY)
1099- connector.connect(quiet=True, max_retries=14)
1100-
1101- self.mocker.result(succeed(None))
1102-
1103- print_text_mock(ARGS)
1104- time_mock.sleep(ANY)
1105- reactor_mock.run()
1106-
1107- print_text_mock(
1108- CONTAINS("There was an error communicating with the "
1109- "Landscape client"),
1110- error=True)
1111- print_text_mock(CONTAINS("This machine will be registered"),
1112- error=True)
1113-
1114- sys_mock.exit(2)
1115- connector.disconnect()
1116- reactor_mock.stop()
1117-
1118- self.mocker.replay()
1119-
1120- config = self.get_config(["-a", "accountname", "--silent"])
1121- return register(config, print_text, sys.exit)
1122-
1123-
1124-class RegisterFunctionNoServiceTest(LandscapeTest):
1125-
1126- def test_register_unknown_error(self):
1127- """
1128- When registration fails because of an unknown error, a message is
1129- printed and the program exits.
1130- """
1131- configuration = LandscapeSetupConfiguration()
1132-
1133- # We'll just mock the remote here to have it raise an exception.
1134- connector_factory = self.mocker.replace(
1135- "landscape.broker.amp.RemoteBrokerConnector", passthrough=False)
1136- remote_broker = self.mocker.mock()
1137-
1138- print_text_mock = self.mocker.replace(print_text)
1139- reactor_mock = self.mocker.patch(FakeReactor)
1140-
1141- # This is unordered. It's just way too much of a pain.
1142- print_text_mock("Please wait... ", "")
1143- time_mock = self.mocker.replace("time")
1144- time_mock.sleep(ANY)
1145- self.mocker.count(1)
1146-
1147- # SNORE
1148- connector = connector_factory(ANY, configuration)
1149- connector.connect(max_retries=0, quiet=True)
1150- self.mocker.result(succeed(remote_broker))
1151- remote_broker.call_on_event(ANY)
1152- self.mocker.result(succeed(None))
1153-
1154- # here it is!
1155- remote_broker.register()
1156- self.mocker.result(fail(ZeroDivisionError))
1157-
1158- print_text_mock(ANY, error=True)
1159-
1160- def check_logged_failure(text, error):
1161- self.assertTrue("ZeroDivisionError" in text)
1162- self.mocker.call(check_logged_failure)
1163- print_text_mock("Unknown error occurred.", error=True)
1164-
1165- # WHOAH DUDE. This waits for callLater(0, reactor.stop).
1166- connector.disconnect()
1167- reactor_mock.stop()
1168- reactor_mock.run()
1169-
1170- self.mocker.replay()
1171-
1172- exit = []
1173- register(configuration, print_text, exit.append, max_retries=0,
1174- reactor=FakeReactor())
1175- self.assertEqual(exit, [2])
1176+ def test_register(self):
1177+ """Is the async machinery wired up properly?"""
1178+
1179+ class FauxFailure(object):
1180+ def getTraceback(self):
1181+ return 'traceback'
1182+
1183+ class FauxReactor(object):
1184+ def run(self):
1185+ self.was_run = True
1186+
1187+ def stop(self, *args):
1188+ self.was_stopped = True
1189+
1190+ reactor = FauxReactor()
1191+ connector = FauxConnector(reactor, self.config)
1192+
1193+ def connector_factory(reactor, config):
1194+ return connector
1195+
1196+ # We pre-seed a success because no actual result will be generated.
1197+ register(self.config, reactor, connector_factory, max_retries=99,
1198+ results=['success'])
1199+ self.assertTrue(reactor.was_run)
1200+ # Only a single callback is registered, it does the real work when a
1201+ # connection is established.
1202+ self.assertTrue(1, len(connector.connection.callbacks))
1203+ self.assertEqual(
1204+ 'got_connection',
1205+ connector.connection.callbacks[0].func.__name__)
1206+ # Should something go wrong, there is an error handler registered.
1207+ self.assertTrue(1, len(connector.connection.errbacks))
1208+ self.assertEqual(
1209+ 'got_error',
1210+ connector.connection.errbacks[0].__name__)
1211+ # We ask for retries because networks aren't reliable.
1212+ self.assertEqual(99, connector.max_retries)
1213+
1214+ def test_got_connection(self):
1215+ """got_connection() adds deferreds and callbacks."""
1216+
1217+ def faux_got_connection(add_result, remote, connector, reactor):
1218+ pass
1219+
1220+ class FauxRemote(object):
1221+ handlers = None
1222+ deferred = None
1223+
1224+ def call_on_event(self, handlers):
1225+ assert not self.handlers, "Called twice"
1226+ self.handlers = handlers
1227+ self.call_on_event_deferred = FauxCallOnEventDeferred()
1228+ return self.call_on_event_deferred
1229+
1230+ def register(self):
1231+ assert not self.deferred, "Called twice"
1232+ self.register_deferred = FauxRegisterDeferred()
1233+ return self.register_deferred
1234+
1235+ class FauxCallOnEventDeferred(object):
1236+ def __init__(self):
1237+ self.callbacks = []
1238+ self.errbacks = []
1239+
1240+ def addCallbacks(self, *funcs, **kws):
1241+ self.callbacks.extend(funcs)
1242+
1243+ class FauxRegisterDeferred(object):
1244+ def __init__(self):
1245+ self.callbacks = []
1246+ self.errbacks = []
1247+
1248+ def addCallback(self, func):
1249+ assert func.__name__ == "got_connection", "Wrong callback."
1250+ self.callbacks.append(faux_got_connection)
1251+ self.gather_results_deferred = GatherResultsDeferred()
1252+ return self.gather_results_deferred
1253+
1254+ def addCallbacks(self, *funcs, **kws):
1255+ self.callbacks.extend(funcs)
1256+
1257+ def addErrback(self, func, *args, **kws):
1258+ self.errbacks.append(func)
1259+ return self
1260+
1261+ class GatherResultsDeferred(object):
1262+ def __init__(self):
1263+ self.callbacks = []
1264+ self.errbacks = []
1265+
1266+ def addCallbacks(self, *funcs, **kws):
1267+ self.callbacks.extend(funcs)
1268+
1269+ faux_connector = FauxConnector(self.reactor, self.config)
1270+
1271+ status_results = []
1272+ faux_remote = FauxRemote()
1273+ results = got_connection(
1274+ status_results.append, faux_connector, self.reactor, faux_remote)
1275+ # We set up two deferreds, one for the RPC call and one for event
1276+ # handlers.
1277+ self.assertEqual(2, len(results.resultList))
1278+ # Handlers are registered for the events we are interested in.
1279+ self.assertEqual(
1280+ ['registration-failed', 'exchange-failed', 'registration-done'],
1281+ faux_remote.handlers.keys())
1282+ self.assertEqual(
1283+ ['failure', 'exchange_failure', 'success'],
1284+ [handler.func.__name__
1285+ for handler in faux_remote.handlers.values()])
1286+ # We include a single error handler to react to exchange errors.
1287+ self.assertTrue(1, len(faux_remote.register_deferred.errbacks))
1288+ self.assertEqual(
1289+ 'handle_registration_errors',
1290+ faux_remote.register_deferred.errbacks[0].__name__)
1291+
1292+ def test_register_happy_path(self):
1293+ """A successful result provokes no exceptions."""
1294+ def faux_got_connection(add_result, remote, connector, reactor):
1295+ add_result('success')
1296+ self.reactor.call_later(1, self.reactor.stop)
1297+ self.assertEqual(
1298+ "success",
1299+ register(self.config, reactor=self.reactor,
1300+ got_connection=faux_got_connection))
1301
1302
1303 class SSLCertificateDataTest(LandscapeConfigurationTest):
1304@@ -2159,3 +2121,41 @@
1305 self.assertEqual(key_filename,
1306 store_public_key_data(config, "123456789"))
1307 self.assertEqual("123456789", open(key_filename, "r").read())
1308+
1309+
1310+class ReportRegistrationOutcomeTest(unittest.TestCase):
1311+
1312+ def setUp(self):
1313+ self.result = []
1314+ self.output = []
1315+
1316+ def record_result(self, result, file=sys.stdout):
1317+ self.result.append(result)
1318+ self.output.append(file.name)
1319+
1320+ def test_success_case(self):
1321+ report_registration_outcome("success", print=self.record_result)
1322+ self.assertIn("System successfully registered.", self.result)
1323+ self.assertIn(sys.stdout.name, self.output)
1324+
1325+ def test_failure_case(self):
1326+ report_registration_outcome("failure", print=self.record_result)
1327+ self.assertIn("Invalid account name or registration key.", self.result)
1328+ self.assertIn(sys.stderr.name, self.output)
1329+
1330+ def test_ssl_error_case(self):
1331+ report_registration_outcome("ssl-error", print=self.record_result)
1332+ self.assertIn("\nThe server's SSL information is incorrect, or fails "
1333+ "signature verification!\n"
1334+ "If the server is using a self-signed certificate, "
1335+ "please ensure you supply it with the --ssl-public-key "
1336+ "parameter.", self.result)
1337+ self.assertIn(sys.stderr.name, self.output)
1338+
1339+ def test_non_ssl_error_case(self):
1340+ report_registration_outcome("non-ssl-error", print=self.record_result)
1341+ self.assertIn("\nWe were unable to contact the server.\n"
1342+ "Your internet connection may be down. "
1343+ "The landscape client will continue to try and contact "
1344+ "the server periodically.", self.result)
1345+ self.assertIn(sys.stderr.name, self.output)

Subscribers

People subscribed via source and target branches

to all changes: