twisted / txacme

Compare 4c39eff ... +28 ... 14f99bc

No flags found

Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.

e.g., #unittest #integration

#production #enterprise

#frontend #backend

Learn more about Codecov Flags here.

Showing 11 of 40 files from the diff.

@@ -29,37 +29,30 @@
Loading
29 29
    raise ValueError(key_type)
30 30
31 31
32 -
def generate_tls_sni_01_cert(server_name, key_type=u'rsa',
33 -
                             _generate_private_key=None):
34 -
    """
35 -
    Generate a certificate/key pair for responding to a tls-sni-01 challenge.
36 -
37 -
    :param str server_name: The SAN the certificate should have.
38 -
    :param str key_type: The type of key to generate; usually not necessary.
39 -
40 -
    :rtype: ``Tuple[`~cryptography.x509.Certificate`, PrivateKey]``
41 -
    :return: A tuple of the certificate and private key.
42 -
    """
43 -
    key = (_generate_private_key or generate_private_key)(key_type)
44 -
    name = x509.Name([
45 -
        x509.NameAttribute(NameOID.COMMON_NAME, u'acme.invalid')])
46 -
    cert = (
47 -
        x509.CertificateBuilder()
48 -
        .subject_name(name)
49 -
        .issuer_name(name)
50 -
        .not_valid_before(datetime.now() - timedelta(seconds=3600))
51 -
        .not_valid_after(datetime.now() + timedelta(seconds=3600))
52 -
        .serial_number(int(uuid.uuid4()))
53 -
        .public_key(key.public_key())
54 -
        .add_extension(
55 -
            x509.SubjectAlternativeName([x509.DNSName(server_name)]),
56 -
            critical=False)
57 -
        .sign(
58 -
            private_key=key,
59 -
            algorithm=hashes.SHA256(),
32 +
def load_or_create_client_key(pem_path):
33 +
    """
34 +
    Load the client key from a directory, creating it if it does not exist.
35 +
36 +
    .. note:: The client key that will be created will be a 2048-bit RSA key.
37 +
38 +
    :type pem_path: ``twisted.python.filepath.FilePath``
39 +
    :param pem_path: The certificate directory
40 +
        to use, as with the endpoint.
41 +
    """
42 +
    acme_key_file = pem_path.asTextMode().child(u'client.key')
43 +
    if acme_key_file.exists():
44 +
        key = serialization.load_pem_private_key(
45 +
            acme_key_file.getContent(),
46 +
            password=None,
60 47
            backend=default_backend())
61 -
        )
62 -
    return (cert, key)
48 +
    else:
49 +
        key = generate_private_key(u'rsa')
50 +
        acme_key_file.setContent(
51 +
            key.private_bytes(
52 +
                encoding=serialization.Encoding.PEM,
53 +
                format=serialization.PrivateFormat.TraditionalOpenSSL,
54 +
                encryption_algorithm=serialization.NoEncryption()))
55 +
    return JWKRSA(key=key)
63 56
64 57
65 58
def tap(f):

@@ -13,7 +13,7 @@
Loading
13 13
14 14
LOG_JWS_SIGN = ActionType(
15 15
    u'txacme:jws:sign',
16 -
    fields(NONCE, key_type=unicode, alg=unicode),
16 +
    fields(NONCE, key_type=unicode, alg=unicode, kid=unicode),
17 17
    fields(),
18 18
    u'Signing a message with JWS')
19 19
@@ -93,27 +93,6 @@
Loading
93 93
                 u'The resulting registration')),
94 94
    u'Registering with an ACME server')
95 95
96 -
LOG_ACME_UPDATE_REGISTRATION = ActionType(
97 -
    u'txacme:acme:client:registration:update',
98 -
    fields(Field(u'registration',
99 -
                 methodcaller('to_json'),
100 -
                 u'An ACME registration'),
101 -
           uri=unicode),
102 -
    fields(Field(u'registration',
103 -
                 methodcaller('to_json'),
104 -
                 u'The updated registration')),
105 -
    u'Updating a registration')
106 -
107 -
LOG_ACME_CREATE_AUTHORIZATION = ActionType(
108 -
    u'txacme:acme:client:authorization:create',
109 -
    fields(Field(u'identifier',
110 -
                 methodcaller('to_json'),
111 -
                 u'An identifier')),
112 -
    fields(Field(u'authorization',
113 -
                 methodcaller('to_json'),
114 -
                 u'The authorization')),
115 -
    u'Creating an authorization')
116 -
117 96
LOG_ACME_ANSWER_CHALLENGE = ActionType(
118 97
    u'txacme:acme:client:challenge:answer',
119 98
    fields(Field(u'challenge_body',
@@ -126,28 +105,3 @@
Loading
126 105
                 methodcaller('to_json'),
127 106
                 u'The updated challenge')),
128 107
    u'Answering an authorization challenge')
129 -
130 -
LOG_ACME_POLL_AUTHORIZATION = ActionType(
131 -
    u'txacme:acme:client:authorization:poll',
132 -
    fields(Field(u'authorization',
133 -
                 methodcaller('to_json'),
134 -
                 u'The authorization resource')),
135 -
    fields(Field(u'authorization',
136 -
                 methodcaller('to_json'),
137 -
                 u'The updated authorization'),
138 -
           Field.for_types(u'retry_after',
139 -
                           [int, float],
140 -
                           u'How long before polling again?')),
141 -
    u'Polling an authorization')
142 -
143 -
LOG_ACME_REQUEST_CERTIFICATE = ActionType(
144 -
    u'txacme:acme:client:certificate:request',
145 -
    fields(),
146 -
    fields(),
147 -
    u'Requesting a certificate')
148 -
149 -
LOG_ACME_FETCH_CHAIN = ActionType(
150 -
    u'txacme:acme:client:certificate:fetch-chain',
151 -
    fields(),
152 -
    fields(),
153 -
    u'Fetching a certificate chain')

@@ -5,224 +5,60 @@
Loading
5 5
6 6
from acme import challenges
7 7
from josepy.b64 import b64encode
8 -
from hypothesis import strategies as s
9 -
from hypothesis import assume, example, given
10 -
from testtools import skipIf, TestCase
11 -
from testtools.matchers import (
12 -
    AfterPreprocessing, Always, Contains, EndsWith, Equals, HasLength,
13 -
    Is, IsInstance, MatchesAll, MatchesListwise, MatchesPredicate,
14 -
    MatchesStructure, Not)
15 -
from testtools.twistedsupport import succeeded
16 8
from treq.testing import StubTreq
17 9
from twisted._threads import createMemoryWorker
18 -
from twisted.internet.defer import maybeDeferred
10 +
from twisted.internet import defer
11 +
from twisted.trial.unittest import TestCase
12 +
19 13
from twisted.python.url import URL
20 14
from twisted.web.resource import Resource
21 15
from zope.interface.verify import verifyObject
22 16
23 -
from txacme.challenges import HTTP01Responder, TLSSNI01Responder
24 -
from txacme.challenges._tls import _MergingMappingProxy
17 +
from txacme.challenges import HTTP01Responder
25 18
from txacme.errors import NotInZone, ZoneNotFound
26 19
from txacme.interfaces import IResponder
27 -
from txacme.test import strategies as ts
28 -
from txacme.test.doubles import SynchronousReactorThreads
29 -
from txacme.test.test_client import failed_with, RSA_KEY_512, RSA_KEY_512_RAW
30 -
31 -
32 -
try:
33 -
    from txacme.challenges import LibcloudDNSResponder
34 -
    from txacme.challenges._libcloud import _daemon_thread
35 -
except ImportError:
36 -
    LibcloudDNSResponder = None
20 +
from txacme.test.test_client import RSA_KEY_512, RSA_KEY_512_RAW
37 21
38 22
39 23
# A random example token for the challenge tests that need one
40 24
EXAMPLE_TOKEN = b'BWYcfxzmOha7-7LoxziqPZIUr99BCz3BfbN9kzSFnrU'
41 25
42 26
43 -
class _CommonResponderTests(object):
27 +
class HTTPResponderTests(TestCase):
44 28
    """
45 -
    Common properties which every responder implementation should satisfy.
29 +
    `.HTTP01Responder` is a responder for http-01 challenges.
46 30
    """
47 -
    def _do_one_thing(self):
48 -
        """
49 -
        Make the underlying fake implementation do one thing (eg.  simulate one
50 -
        network request, one threaded task execution).
51 -
        """
52 31
53 32
    def test_interface(self):
54 33
        """
55 34
        The `.IResponder` interface is correctly implemented.
56 35
        """
57 -
        responder = self._responder_factory()
36 +
        responder = HTTP01Responder()
58 37
        verifyObject(IResponder, responder)
59 -
        self.assertThat(responder.challenge_type, Equals(self._challenge_type))
38 +
        self.assertEqual(u'http-01', responder.challenge_type)
60 39
61 -
    @example(token=EXAMPLE_TOKEN)
62 -
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode))
63 -
    def test_stop_responding_already_stopped(self, token):
40 +
    @defer.inlineCallbacks
41 +
    def test_stop_responding_already_stopped(self):
64 42
        """
65 43
        Calling ``stop_responding`` when we are not responding for a server
66 44
        name does nothing.
67 45
        """
68 -
        challenge = self._challenge_factory(token=token)
46 +
        token = EXAMPLE_TOKEN
47 +
        challenge = challenges.HTTP01(token=token)
69 48
        response = challenge.response(RSA_KEY_512)
70 -
        responder = self._responder_factory()
71 -
        d = maybeDeferred(
72 -
            responder.stop_responding,
49 +
        responder = HTTP01Responder()
50 +
51 +
        yield responder.stop_responding(
73 52
            u'example.com',
74 53
            challenge,
75 54
            response)
76 -
        self._do_one_thing()
77 -
        self.assertThat(d, succeeded(Always()))
78 -
79 55
80 -
class TLSResponderTests(_CommonResponderTests, TestCase):
81 -
    """
82 -
    `.TLSSNI01Responder` is a responder for tls-sni-01 challenges that works
83 -
    with txsni.
84 -
    """
85 -
    _challenge_factory = challenges.TLSSNI01
86 -
    _responder_factory = TLSSNI01Responder
87 -
    _challenge_type = u'tls-sni-01'
88 -
89 -
    @example(token=b'BWYcfxzmOha7-7LoxziqPZIUr99BCz3BfbN9kzSFnrU')
90 -
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode))
91 -
    def test_start_responding(self, token):
92 -
        """
93 -
        Calling ``start_responding`` makes an appropriate entry appear in the
94 -
        host map.
95 -
        """
96 -
        ckey = RSA_KEY_512_RAW
97 -
        challenge = challenges.TLSSNI01(token=token)
98 -
        response = challenge.response(RSA_KEY_512)
99 -
        server_name = response.z_domain.decode('ascii')
100 -
        host_map = {}
101 -
        responder = TLSSNI01Responder()
102 -
        responder._generate_private_key = lambda key_type: ckey
103 -
        wrapped_host_map = responder.wrap_host_map(host_map)
104 -
105 -
        self.assertThat(wrapped_host_map, Not(Contains(server_name)))
106 -
        responder.start_responding(u'example.com', challenge, response)
107 -
        self.assertThat(
108 -
            wrapped_host_map.get(server_name.encode('utf-8')).certificate,
109 -
            MatchesPredicate(response.verify_cert, '%r does not verify'))
110 -
111 -
        # Starting twice before stopping doesn't break things
112 -
        responder.start_responding(u'example.com', challenge, response)
113 -
        self.assertThat(
114 -
            wrapped_host_map.get(server_name.encode('utf-8')).certificate,
115 -
            MatchesPredicate(response.verify_cert, '%r does not verify'))
116 -
117 -
        responder.stop_responding(u'example.com', challenge, response)
118 -
        self.assertThat(wrapped_host_map, Not(Contains(server_name)))
119 -
120 -
121 -
class MergingProxyTests(TestCase):
122 -
    """
123 -
    ``_MergingMappingProxy`` merges two mappings together.
124 -
    """
125 -
    @example(underlay={}, overlay={}, key=u'foo')
126 -
    @given(underlay=s.dictionaries(s.text(), s.builds(object)),
127 -
           overlay=s.dictionaries(s.text(), s.builds(object)),
128 -
           key=s.text())
129 -
    def test_get_overlay(self, underlay, overlay, key):
130 -
        """
131 -
        Getting an key that only exists in the overlay returns the value from
132 -
        the overlay.
133 -
        """
134 -
        underlay.pop(key, None)
135 -
        overlay[key] = object()
136 -
        proxy = _MergingMappingProxy(
137 -
            overlay=overlay, underlay=underlay)
138 -
        self.assertThat(proxy[key], Is(overlay[key]))
139 -
140 -
    @example(underlay={}, overlay={}, key=u'foo')
141 -
    @given(underlay=s.dictionaries(s.text(), s.builds(object)),
142 -
           overlay=s.dictionaries(s.text(), s.builds(object)),
143 -
           key=s.text())
144 -
    def test_get_underlay(self, underlay, overlay, key):
145 -
        """
146 -
        Getting an key that only exists in the underlay returns the value from
147 -
        the underlay.
148 -
        """
149 -
        underlay[key] = object()
150 -
        overlay.pop(key, None)
151 -
        proxy = _MergingMappingProxy(
152 -
            overlay=overlay, underlay=underlay)
153 -
        self.assertThat(proxy[key], Is(underlay[key]))
154 -
155 -
    @example(underlay={}, overlay={}, key=u'foo')
156 -
    @given(underlay=s.dictionaries(s.text(), s.builds(object)),
157 -
           overlay=s.dictionaries(s.text(), s.builds(object)),
158 -
           key=s.text())
159 -
    def test_get_both(self, underlay, overlay, key):
160 -
        """
161 -
        Getting an key that exists in both the underlay and the overlay returns
162 -
        the value from the overlay.
163 -
        """
164 -
        underlay[key] = object()
165 -
        overlay[key] = object()
166 -
        proxy = _MergingMappingProxy(
167 -
            overlay=overlay, underlay=underlay)
168 -
        self.assertThat(proxy[key], Not(Is(underlay[key])))
169 -
        self.assertThat(proxy[key], Is(overlay[key]))
170 -
171 -
    @example(underlay={u'foo': object(), u'bar': object()},
172 -
             overlay={u'bar': object(), u'baz': object()})
173 -
    @given(underlay=s.dictionaries(s.text(), s.builds(object)),
174 -
           overlay=s.dictionaries(s.text(), s.builds(object)))
175 -
    def test_len(self, underlay, overlay):
176 -
        """
177 -
        ``__len__`` of the proxy does not count duplicates.
178 -
        """
179 -
        proxy = _MergingMappingProxy(
180 -
            overlay=overlay, underlay=underlay)
181 -
        self.assertThat(len(proxy), Equals(len(list(proxy))))
182 -
183 -
    @example(underlay={u'foo': object(), u'bar': object()},
184 -
             overlay={u'bar': object(), u'baz': object()})
185 -
    @given(underlay=s.dictionaries(s.text(), s.builds(object)),
186 -
           overlay=s.dictionaries(s.text(), s.builds(object)))
187 -
    def test_iter(self, underlay, overlay):
188 -
        """
189 -
        ``__iter__`` of the proxy does not produce duplicate keys.
190 -
        """
191 -
        proxy = _MergingMappingProxy(
192 -
            overlay=overlay, underlay=underlay)
193 -
        keys = sorted(list(proxy))
194 -
        self.assertThat(keys, Equals(sorted(list(set(keys)))))
195 -
196 -
    @example(underlay={u'foo': object()}, overlay={}, key=u'foo')
197 -
    @example(underlay={}, overlay={}, key=u'bar')
198 -
    @given(underlay=s.dictionaries(s.text(), s.builds(object)),
199 -
           overlay=s.dictionaries(s.text(), s.builds(object)),
200 -
           key=s.text())
201 -
    def test_contains(self, underlay, overlay, key):
202 -
        """
203 -
        The mapping only contains a key if it can be gotten.
204 -
        """
205 -
        proxy = _MergingMappingProxy(
206 -
            overlay=overlay, underlay=underlay)
207 -
        self.assertThat(
208 -
            key in proxy,
209 -
            Equals(proxy.get(key) is not None))
210 -
211 -
212 -
class HTTPResponderTests(_CommonResponderTests, TestCase):
213 -
    """
214 -
    `.HTTP01Responder` is a responder for http-01 challenges.
215 -
    """
216 -
    _challenge_factory = challenges.HTTP01
217 -
    _responder_factory = HTTP01Responder
218 -
    _challenge_type = u'http-01'
219 -
220 -
    @example(token=b'BWYcfxzmOha7-7LoxziqPZIUr99BCz3BfbN9kzSFnrU')
221 -
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode))
222 -
    def test_start_responding(self, token):
56 +
    @defer.inlineCallbacks
57 +
    def test_start_responding(self):
223 58
        """
224 59
        Calling ``start_responding`` makes an appropriate resource available.
225 60
        """
61 +
        token = b'BWYcfxzmOha7-7LoxziqPZIUr99BCz3BfbN9kzSFnrU'
226 62
        challenge = challenges.HTTP01(token=token)
227 63
        response = challenge.response(RSA_KEY_512)
228 64
@@ -238,240 +74,33 @@
Loading
238 74
        challenge_url = URL(host=u'example.com', path=[
239 75
            u'.well-known', u'acme-challenge', encoded_token]).asText()
240 76
241 -
        self.assertThat(client.get(challenge_url),
242 -
                        succeeded(MatchesStructure(code=Equals(404))))
243 -
244 -
        responder.start_responding(u'example.com', challenge, response)
245 -
        self.assertThat(client.get(challenge_url), succeeded(MatchesAll(
246 -
            MatchesStructure(
247 -
                code=Equals(200),
248 -
                headers=AfterPreprocessing(
249 -
                    methodcaller('getRawHeaders', b'content-type'),
250 -
                    Equals([b'text/plain']))),
251 -
            AfterPreprocessing(methodcaller('content'), succeeded(
252 -
                Equals(response.key_authorization.encode('utf-8'))))
253 -
        )))
77 +
        # We got page not found while the challenge is not yet active.
78 +
        result = yield client.get(challenge_url)
79 +
        self.assertEqual(404, result.code)
254 80
255 -
        # Starting twice before stopping doesn't break things
81 +
        # Once we enable the response.
256 82
        responder.start_responding(u'example.com', challenge, response)
257 -
        self.assertThat(client.get(challenge_url),
258 -
                        succeeded(MatchesStructure(code=Equals(200))))
259 -
260 -
        responder.stop_responding(u'example.com', challenge, response)
261 -
        self.assertThat(client.get(challenge_url),
262 -
                        succeeded(MatchesStructure(code=Equals(404))))
263 -
264 -
265 -
@skipIf(LibcloudDNSResponder is None, 'libcloud not available')
266 -
class LibcloudResponderTests(_CommonResponderTests, TestCase):
267 -
    """
268 -
    `.LibcloudDNSResponder` implements a responder for dns-01 challenges using
269 -
    libcloud on the backend.
270 -
    """
271 -
    _challenge_factory = challenges.DNS01
272 -
    _challenge_type = u'dns-01'
273 -
274 -
    def _responder_factory(self, zone_name=u'example.com'):
275 -
        responder = LibcloudDNSResponder.create(
276 -
            reactor=SynchronousReactorThreads(),
277 -
            driver_name='dummy',
278 -
            username='ignored',
279 -
            password='ignored',
280 -
            zone_name=zone_name,
281 -
            settle_delay=0.0)
282 -
        if zone_name is not None:
283 -
            responder._driver.create_zone(zone_name)
284 -
        responder._thread_pool, self._perform = createMemoryWorker()
285 -
        return responder
286 -
287 -
    def _do_one_thing(self):
288 -
        return self._perform()
289 -
290 -
    def test_daemon_threads(self):
291 -
        """
292 -
        ``_daemon_thread`` creates thread objects with ``daemon`` set.
293 -
        """
294 -
        thread = _daemon_thread()
295 -
        self.assertThat(thread, MatchesStructure(daemon=Equals(True)))
296 -
297 -
    @example(token=EXAMPLE_TOKEN,
298 -
             subdomain=u'acme-testing',
299 -
             zone_name=u'example.com')
300 -
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
301 -
           subdomain=ts.dns_names(),
302 -
           zone_name=ts.dns_names())
303 -
    def test_start_responding(self, token, subdomain, zone_name):
304 -
        """
305 -
        Calling ``start_responding`` causes an appropriate TXT record to be
306 -
        created.
307 -
        """
308 -
        challenge = self._challenge_factory(token=token)
309 -
        response = challenge.response(RSA_KEY_512)
310 -
        responder = self._responder_factory(zone_name=zone_name)
311 -
        server_name = u'{}.{}'.format(subdomain, zone_name)
312 -
        zone = responder._driver.list_zones()[0]
83 +
        result = yield client.get(challenge_url)
84 +
        self.assertEqual(200, result.code)
85 +
        self.assertEqual(
86 +
            ['text/plain'], result.headers.getRawHeaders('content-type'))
313 87
314 -
        self.assertThat(zone.list_records(), HasLength(0))
315 -
        d = responder.start_responding(server_name, challenge, response)
316 -
        self._perform()
317 -
        self.assertThat(d, succeeded(Always()))
318 -
        self.assertThat(
319 -
            zone.list_records(),
320 -
            MatchesListwise([
321 -
                MatchesStructure(
322 -
                    name=EndsWith(u'.' + subdomain),
323 -
                    type=Equals('TXT'),
324 -
                    )]))
88 +
        result = yield result.content()
89 +
        self.assertEqual(response.key_authorization.encode('utf-8'), result)
325 90
326 91
        # Starting twice before stopping doesn't break things
327 -
        d = responder.start_responding(server_name, challenge, response)
328 -
        self._perform()
329 -
        self.assertThat(d, succeeded(Always()))
330 -
        self.assertThat(zone.list_records(), HasLength(1))
331 -
332 -
        d = responder.stop_responding(server_name, challenge, response)
333 -
        self._perform()
334 -
        self.assertThat(d, succeeded(Always()))
335 -
        self.assertThat(zone.list_records(), HasLength(0))
92 +
        responder.start_responding(u'example.com', challenge, response)
336 93
337 -
    @example(token=EXAMPLE_TOKEN,
338 -
             subdomain=u'acme-testing',
339 -
             zone_name=u'example.com')
340 -
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
341 -
           subdomain=ts.dns_names(),
342 -
           zone_name=ts.dns_names())
343 -
    def test_wrong_zone(self, token, subdomain, zone_name):
344 -
        """
345 -
        Trying to respond for a domain not in the configured zone results in a
346 -
        `.NotInZone` exception.
347 -
        """
348 -
        challenge = self._challenge_factory(token=token)
349 -
        response = challenge.response(RSA_KEY_512)
350 -
        responder = self._responder_factory(zone_name=zone_name)
351 -
        server_name = u'{}.{}.junk'.format(subdomain, zone_name)
352 -
        d = maybeDeferred(
353 -
            responder.start_responding, server_name, challenge, response)
354 -
        self._perform()
355 -
        self.assertThat(
356 -
            d,
357 -
            failed_with(MatchesAll(
358 -
                IsInstance(NotInZone),
359 -
                MatchesStructure(
360 -
                    server_name=EndsWith(u'.' + server_name),
361 -
                    zone_name=Equals(zone_name)))))
94 +
        result = yield client.get(challenge_url)
95 +
        self.assertEqual(200, result.code)
362 96
363 -
    @example(token=EXAMPLE_TOKEN,
364 -
             subdomain=u'acme-testing',
365 -
             zone_name=u'example.com')
366 -
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
367 -
           subdomain=ts.dns_names(),
368 -
           zone_name=ts.dns_names())
369 -
    def test_missing_zone(self, token, subdomain, zone_name):
370 -
        """
371 -
        `.ZoneNotFound` is raised if the configured zone cannot be found at the
372 -
        configured provider.
373 -
        """
374 -
        challenge = self._challenge_factory(token=token)
375 -
        response = challenge.response(RSA_KEY_512)
376 -
        responder = self._responder_factory(zone_name=zone_name)
377 -
        server_name = u'{}.{}'.format(subdomain, zone_name)
378 -
        for zone in responder._driver.list_zones():
379 -
            zone.delete()
380 -
        d = maybeDeferred(
381 -
            responder.start_responding, server_name, challenge, response)
382 -
        self._perform()
383 -
        self.assertThat(
384 -
            d,
385 -
            failed_with(MatchesAll(
386 -
                IsInstance(ZoneNotFound),
387 -
                MatchesStructure(
388 -
                    zone_name=Equals(zone_name)))))
97 +
        yield responder.stop_responding(u'example.com', challenge, response)
389 98
390 -
    @example(token=EXAMPLE_TOKEN,
391 -
             subdomain=u'acme-testing',
392 -
             extra=u'extra',
393 -
             zone_name1=u'example.com',
394 -
             suffix1=u'.',
395 -
             zone_name2=u'example.org',
396 -
             suffix2=u'')
397 -
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
398 -
           subdomain=ts.dns_names(),
399 -
           extra=ts.dns_names(),
400 -
           zone_name1=ts.dns_names(),
401 -
           suffix1=s.sampled_from([u'', u'.']),
402 -
           zone_name2=ts.dns_names(),
403 -
           suffix2=s.sampled_from([u'', u'.']))
404 -
    def test_auto_zone(self, token, subdomain, extra, zone_name1, suffix1,
405 -
                       zone_name2, suffix2):
406 -
        """
407 -
        If the configured zone_name is ``None``, the zone will be guessed by
408 -
        finding the longest zone that is a suffix of the server name.
409 -
        """
410 -
        zone_name3 = extra + u'.' + zone_name1
411 -
        zone_name4 = extra + u'.' + zone_name2
412 -
        server_name = u'{}.{}.{}'.format(subdomain, extra, zone_name1)
413 -
        assume(
414 -
            len({server_name, zone_name1, zone_name2, zone_name3, zone_name4})
415 -
            == 5)
416 -
        challenge = self._challenge_factory(token=token)
417 -
        response = challenge.response(RSA_KEY_512)
418 -
        responder = self._responder_factory(zone_name=None)
419 -
        zone1 = responder._driver.create_zone(zone_name1 + suffix1)
420 -
        zone2 = responder._driver.create_zone(zone_name2 + suffix2)
421 -
        zone3 = responder._driver.create_zone(zone_name3 + suffix1)
422 -
        zone4 = responder._driver.create_zone(zone_name4 + suffix2)
423 -
        self.assertThat(zone1.list_records(), HasLength(0))
424 -
        self.assertThat(zone2.list_records(), HasLength(0))
425 -
        self.assertThat(zone3.list_records(), HasLength(0))
426 -
        self.assertThat(zone4.list_records(), HasLength(0))
427 -
        d = responder.start_responding(server_name, challenge, response)
428 -
        self._perform()
429 -
        self.assertThat(d, succeeded(Always()))
430 -
        self.assertThat(zone1.list_records(), HasLength(0))
431 -
        self.assertThat(zone2.list_records(), HasLength(0))
432 -
        self.assertThat(
433 -
            zone3.list_records(),
434 -
            MatchesListwise([
435 -
                MatchesStructure(
436 -
                    name=AfterPreprocessing(
437 -
                        methodcaller('rstrip', u'.'),
438 -
                        EndsWith(u'.' + subdomain)),
439 -
                    type=Equals('TXT'),
440 -
                    )]))
441 -
        self.assertThat(zone4.list_records(), HasLength(0))
99 +
        result = yield client.get(challenge_url)
100 +
        self.assertEqual(404, result.code)
442 101
443 -
    @example(token=EXAMPLE_TOKEN,
444 -
             subdomain=u'acme-testing',
445 -
             zone_name1=u'example.com',
446 -
             zone_name2=u'example.org')
447 -
    @given(token=s.binary(min_size=32, max_size=32).map(b64encode),
448 -
           subdomain=ts.dns_names(),
449 -
           zone_name1=ts.dns_names(),
450 -
           zone_name2=ts.dns_names())
451 -
    def test_auto_zone_missing(self, token, subdomain, zone_name1, zone_name2):
452 -
        """
453 -
        If the configured zone_name is ``None``, and no matching zone is found,
454 -
        ``NotInZone`` is raised.
455 -
        """
456 -
        server_name = u'{}.{}'.format(subdomain, zone_name1)
457 -
        assume(not server_name.endswith(zone_name2))
458 -
        challenge = self._challenge_factory(token=token)
459 -
        response = challenge.response(RSA_KEY_512)
460 -
        responder = self._responder_factory(zone_name=None)
461 -
        zone = responder._driver.create_zone(zone_name2)
462 -
        self.assertThat(zone.list_records(), HasLength(0))
463 -
        d = maybeDeferred(
464 -
            responder.start_responding, server_name, challenge, response)
465 -
        self._perform()
466 -
        self.assertThat(
467 -
            d,
468 -
            failed_with(MatchesAll(
469 -
                IsInstance(NotInZone),
470 -
                MatchesStructure(
471 -
                    server_name=EndsWith(u'.' + server_name),
472 -
                    zone_name=Is(None)))))
473 102
474 103
475 104
__all__ = [
476 105
    'HTTPResponderTests', 'TLSResponderTests', 'MergingProxyTests',
477 -
    'LibcloudResponderTests']
106 +
    ]

@@ -13,20 +13,11 @@
Loading
13 13
from cryptography.hazmat.backends import default_backend
14 14
from cryptography.hazmat.primitives import serialization
15 15
from cryptography.hazmat.primitives.asymmetric import rsa
16 -
from fixtures import Fixture
17 -
from hypothesis import strategies as s
18 -
from hypothesis import assume, example, given, settings
19 -
from testtools import ExpectedException, TestCase
20 -
from testtools.matchers import (
21 -
    AfterPreprocessing, Always, ContainsDict, Equals, Is, IsInstance,
22 -
    MatchesAll, MatchesListwise, MatchesPredicate, MatchesStructure, Mismatch,
23 -
    Never, Not, StartsWith, HasLength)
24 -
from testtools.twistedsupport import failed, succeeded, has_no_result
25 16
from treq.client import HTTPClient
26 17
from treq.testing import RequestSequence as treq_RequestSequence
27 18
from treq.testing import (
28 19
    _SynchronousProducer, RequestTraversalAgent, StringStubbingResource)
29 -
from twisted.internet import reactor
20 +
from twisted.internet import defer, reactor
30 21
from twisted.internet.defer import Deferred, CancelledError, fail, succeed
31 22
from twisted.internet.error import ConnectionClosed
32 23
from twisted.internet.task import Clock
@@ -35,19 +26,22 @@
Loading
35 26
from twisted.web import http, server
36 27
from twisted.web.resource import Resource
37 28
from twisted.web.http_headers import Headers
29 +
from twisted.trial.unittest import TestCase
38 30
from zope.interface import implementer
39 31
40 32
from txacme.client import (
41 33
    _default_client, _find_supported_challenge, _parse_header_links,
42 34
    answer_challenge, AuthorizationFailed, Client, DER_CONTENT_TYPE,
43 -
    fqdn_identifier, JSON_CONTENT_TYPE, JSON_ERROR_CONTENT_TYPE, JWSClient,
44 -
    NoSupportedChallenges, poll_until_valid, ServerError)
35 +
    fqdn_identifier, JSON_CONTENT_TYPE, JOSE_CONTENT_TYPE,
36 +
    JSON_ERROR_CONTENT_TYPE, JWSClient, NoSupportedChallenges, ServerError,
37 +
    get_certificate
38 +
)
45 39
from txacme.interfaces import IResponder
46 40
from txacme.messages import CertificateRequest
47 -
from txacme.test import strategies as ts
48 41
from txacme.testing import NullResponder
49 42
from txacme.util import (
50 -
    csr_for_names, generate_private_key, generate_tls_sni_01_cert)
43 +
    csr_for_names, generate_private_key
44 +
)
51 45
52 46
53 47
def failed_with(matcher):
@@ -89,116 +83,6 @@
Loading
89 83
RSA_KEY_512 = JWKRSA(key=RSA_KEY_512_RAW)
90 84
91 85
92 -
class Nearly(object):
93 -
    """Within a certain threshold."""
94 -
    def __init__(self, expected, epsilon=0.001):
95 -
        self.expected = expected
96 -
        self.epsilon = epsilon
97 -
98 -
    def __str__(self):
99 -
        return 'Nearly(%r, %r)' % (self.expected, self.epsilon)
100 -
101 -
    def match(self, value):
102 -
        if abs(value - self.expected) > self.epsilon:
103 -
            return Mismatch(
104 -
                u'%r more than %r from %r' % (
105 -
                    value, self.epsilon, self.expected))
106 -
107 -
108 -
@attr.s(auto_attribs=True)
109 -
class ConnectionPoolFixture:
110 -
    _deferred: Deferred = attr.Factory(Deferred)
111 -
    _closing: bool = False
112 -
113 -
    def started_closing(self):
114 -
        return self._closing
115 -
116 -
    def finish_closing(self):
117 -
        self._deferred.callback(None)
118 -
119 -
120 -
@attr.s(auto_attribs=True)
121 -
class FakePool:
122 -
    _fixture_pool: ConnectionPoolFixture
123 -
124 -
    def closeCachedConnections(self):  # noqa
125 -
        self._fixture_pool._closing = True
126 -
        return self._fixture_pool._deferred
127 -
128 -
129 -
class ClientFixture(Fixture):
130 -
    """
131 -
    Create a :class:`~txacme.client.Client` for testing.
132 -
    """
133 -
    def __init__(
134 -
        self, sequence, key=None, alg=RS256, use_connection_pool=False
135 -
    ):
136 -
        super(ClientFixture, self).__init__()
137 -
        self._sequence = sequence
138 -
        if isinstance(sequence, treq_RequestSequence):
139 -
            self._agent = RequestTraversalAgent(
140 -
                StringStubbingResource(self._sequence)
141 -
            )
142 -
        else:
143 -
            self._agent = RequestTraversalAgent(sequence)
144 -
        if use_connection_pool:
145 -
            self.pool = ConnectionPoolFixture()
146 -
            self._agent._pool = FakePool(self.pool)
147 -
        self._directory = messages.Directory({
148 -
            messages.NewRegistration:
149 -
            u'https://example.org/acme/new-reg',
150 -
            messages.Revocation:
151 -
            u'https://example.org/acme/revoke-cert',
152 -
            messages.NewAuthorization:
153 -
            u'https://example.org/acme/new-authz',
154 -
            messages.CertificateRequest:
155 -
            u'https://example.org/acme/new-cert',
156 -
            })
157 -
        if key is None:
158 -
            key = JWKRSA(key=generate_private_key('rsa'))
159 -
        self._key = key
160 -
        self._alg = alg
161 -
162 -
    def _setUp(self):  # noqa
163 -
        jws_client = JWSClient(self._agent, self._key, self._alg)
164 -
        jws_client._treq._data_to_body_producer = _SynchronousProducer
165 -
        self.clock = Clock()
166 -
        self.client = Client(
167 -
            self._directory, self.clock, self._key,
168 -
            jws_client=jws_client)
169 -
170 -
    def flush(self):
171 -
        self._agent.flush()
172 -
173 -
174 -
def _nonce_response(url, nonce):
175 -
    """
176 -
    Construct an expected request for an initial nonce check.
177 -
178 -
    :param bytes url: The url being requested.
179 -
    :param bytes nonce: The nonce to return.
180 -
181 -
    :return: A request/response tuple suitable for use with
182 -
        :class:`~treq.testing.RequestSequence`.
183 -
    """
184 -
    return (
185 -
        MatchesListwise([
186 -
            Equals(b'HEAD'),
187 -
            Equals(url),
188 -
            Equals({}),
189 -
            ContainsDict({b'User-Agent':
190 -
                          MatchesListwise([StartsWith(b'txacme/')])}),
191 -
            Equals(b'')]),
192 -
        (http.NOT_ALLOWED,
193 -
         {b'content-type': JSON_CONTENT_TYPE,
194 -
          b'replay-nonce': b64encode(nonce)},
195 -
         b'{}'))
196 -
197 -
198 -
def _json_dumps(j):
199 -
    return json.dumps(j).encode("utf-8")
200 -
201 -
202 86
class RequestSequence(treq_RequestSequence):
203 87
    @contextmanager
204 88
    def consume(self, sync_failure_reporter):
@@ -291,1458 +175,76 @@
Loading
291 175
    """
292 176
    :class:`.Client` provides a client interface for the ACME API.
293 177
    """
178 +
179 +
    @defer.inlineCallbacks
294 180
    def test_directory_url_type(self):
295 181
        """
296 182
        `~txacme.client.Client.from_url` expects a ``twisted.python.url.URL``
297 183
        instance for the ``url`` argument.
298 184
        """
299 -
        with ExpectedException(TypeError):
300 -
            Client.from_url(
185 +
        with self.assertRaises(TypeError):
186 +
            yield Client.from_url(
301 187
                reactor, '/wrong/kind/of/directory', key=RSA_KEY_512)
302 188
303 -
    def test_register_missing_next(self):
304 -
        """
305 -
        If the directory does not return a ``"next"`` link, a
306 -
        :exc:`~acme.errors.ClientError` failure occurs.
307 -
        """
308 -
        sequence = RequestSequence(
309 -
            [_nonce_response(
310 -
                u'https://example.org/acme/new-reg',
311 -
                b'Nonce'),
312 -
             (MatchesListwise([
313 -
                 Equals(b'POST'),
314 -
                 Equals(u'https://example.org/acme/new-reg'),
315 -
                 Equals({}),
316 -
                 Always(),
317 -
                 Always()]),
318 -
              (http.CREATED,
319 -
               {b'content-type': JSON_CONTENT_TYPE,
320 -
                b'replay-nonce': b64encode(b'Nonce2')},
321 -
               b'{}'))],
322 -
            self.expectThat)
323 -
        client = self.useFixture(ClientFixture(sequence)).client
324 -
        with sequence.consume(self.fail):
325 -
            d = client.register()
326 -
        self.expectThat(
327 -
            d, failed_with(MatchesAll(
328 -
                IsInstance(errors.ClientError),
329 -
                AfterPreprocessing(str, Equals('"next" link missing')))))
330 -
331 -
    def test_unexpected_update(self):
332 -
        """
333 -
        If the server does not return the registration we expected, an
334 -
        :exc:`~acme.errors.UnexpectedUpdate` failure occurs.
335 -
        """
336 -
        update = (
337 -
            MatchesListwise([
338 -
                Equals(b'POST'),
339 -
                Equals(u'https://example.org/acme/new-reg'),
340 -
                Equals({}),
341 -
                ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
342 -
                Always()]),
343 -
            (http.CREATED,
344 -
             {b'content-type': JSON_CONTENT_TYPE,
345 -
              b'replay-nonce': b64encode(b'Nonce2'),
346 -
              b'location': b'https://example.org/acme/reg/1',
347 -
              b'link': b','.join([
348 -
                  b'<https://example.org/acme/new-authz>;rel="next"',
349 -
                  b'<https://example.org/acme/recover-reg>;rel="recover"',
350 -
                  b'<https://example.org/acme/terms>;rel="terms-of-service"',
351 -
              ])},
352 -
             _json_dumps({
353 -
                 u'key': {
354 -
                     u'n': u'alQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
355 -
                           u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
356 -
                     u'e': u'AQAB',
357 -
                     u'kty': u'RSA'},
358 -
                 u'contact': [u'mailto:example@example.com'],
359 -
             })))
360 -
        sequence = RequestSequence(
361 -
            [_nonce_response(
362 -
                u'https://example.org/acme/new-reg',
363 -
                b'Nonce'),
364 -
             update,
365 -
             update],
366 -
            self.expectThat)
367 -
        client = self.useFixture(
368 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
369 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
370 -
        reg2 = messages.NewRegistration.from_data(email=u'foo@example.com')
371 -
        with sequence.consume(self.fail):
372 -
            self.assertThat(
373 -
                client.register(reg),
374 -
                failed_with(IsInstance(errors.UnexpectedUpdate)))
375 -
            self.assertThat(
376 -
                client.register(reg2),
377 -
                failed_with(IsInstance(errors.UnexpectedUpdate)))
378 -
        self.expectThat(client.stop(), succeeded(Equals(None)))
379 -
380 -
    def stop_in_progress(self, use_pool=False):
381 -
        requested = []
382 -
383 -
        class NoAnswerResource(Resource):
384 -
            isLeaf = True       # noqa
385 -
386 -
            def render(self, request):
387 -
                requested.append(request.notifyFinish())
388 -
                return server.NOT_DONE_YET
389 -
390 -
        self.client_fixture = self.useFixture(
391 -
            ClientFixture(
392 -
                NoAnswerResource(),
393 -
                key=RSA_KEY_512,
394 -
                use_connection_pool=use_pool,
395 -
            )
396 -
        )
397 -
        client = self.client_fixture.client
398 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
399 -
        register_call = client.register(reg)
400 -
        self.expectThat(requested, HasLength(1))
401 -
        self.expectThat(register_call, has_no_result())
402 -
        self.expectThat(requested[0], has_no_result())
403 -
        stop_deferred = client.stop()
404 -
        self.assertThat(register_call, succeeded(Equals(None)))
405 -
        self.client_fixture.flush()
406 -
        self.assertThat(
407 -
            requested[0],
408 -
            failed_with(IsInstance(ConnectionClosed)),
409 -
        )
410 -
        return stop_deferred
411 -
412 -
    def test_stop_in_progress(self):
413 -
        """
414 -
        If we stop the client while an operation is in progress, it's
415 -
        cancelled.
416 -
        """
417 -
        self.assertThat(self.stop_in_progress(), succeeded(Equals(None)))
418 -
419 -
    def test_stop_in_progress_with_pool(self):
420 -
        """
421 -
        If we stop the client while an operation is in progress, it will stop.
422 -
        """
423 -
        stopped = self.stop_in_progress(True)
424 -
        self.assertThat(stopped, has_no_result())
425 -
        self.assertThat(self.client_fixture.pool.started_closing(),
426 -
                        Equals(True))
427 -
        self.client_fixture.pool.finish_closing()
428 -
        self.assertThat(stopped, succeeded(Equals(None)))
429 -
430 -
    def test_register(self):
431 -
        """
432 -
        If the registration succeeds, the new registration is returned.
433 -
        """
434 -
        sequence = RequestSequence(
435 -
            [_nonce_response(
436 -
                u'https://example.org/acme/new-reg',
437 -
                b'Nonce'),
438 -
             (MatchesListwise([
439 -
                 Equals(b'POST'),
440 -
                 Equals(u'https://example.org/acme/new-reg'),
441 -
                 Equals({}),
442 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
443 -
                 on_jws(Equals({
444 -
                     u'resource': u'new-reg',
445 -
                     u'contact': [u'mailto:example@example.com']}))]),
446 -
              (http.CREATED,
447 -
               {b'content-type': JSON_CONTENT_TYPE,
448 -
                b'replay-nonce': b64encode(b'Nonce2'),
449 -
                b'location': b'https://example.org/acme/reg/1',
450 -
                b'link': b','.join([
451 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
452 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
453 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
454 -
                ])},
455 -
               _json_dumps({
456 -
                   u'key': {
457 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
458 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
459 -
                       u'e': u'AQAB',
460 -
                       u'kty': u'RSA'},
461 -
                   u'contact': [u'mailto:example@example.com'],
462 -
               })))],
463 -
            self.expectThat)
464 -
        client = self.useFixture(
465 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
466 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
467 -
        with sequence.consume(self.fail):
468 -
            d = client.register(reg)
469 -
            self.assertThat(
470 -
                d, succeeded(MatchesStructure(
471 -
                    body=MatchesStructure(
472 -
                        key=Equals(RSA_KEY_512.public_key()),
473 -
                        contact=Equals(reg.contact)),
474 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
475 -
                    new_authzr_uri=Equals(
476 -
                        u'https://example.org/acme/new-authz'),
477 -
                    terms_of_service=Equals(u'https://example.org/acme/terms'),
478 -
                )))
479 -
480 -
    def test_register_existing(self):
481 -
        """
482 -
        If registration fails due to our key already being registered, the
483 -
        existing registration is returned.
484 -
        """
485 -
        sequence = RequestSequence(
486 -
            [_nonce_response(
487 -
                u'https://example.org/acme/new-reg',
488 -
                b'Nonce'),
489 -
             (MatchesListwise([
490 -
                 Equals(b'POST'),
491 -
                 Equals(u'https://example.org/acme/new-reg'),
492 -
                 Equals({}),
493 -
                 Always(),
494 -
                 on_jws(Equals({
495 -
                     u'resource': u'new-reg',
496 -
                     u'contact': [u'mailto:example@example.com']}))]),
497 -
              (http.CONFLICT,
498 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
499 -
                b'replay-nonce': b64encode(b'Nonce2'),
500 -
                b'location': b'https://example.org/acme/reg/1',
501 -
                },
502 -
               _json_dumps(
503 -
                   {u'status': http.CONFLICT,
504 -
                    u'type': u'urn:acme:error:malformed',
505 -
                    u'detail': u'Registration key is already in use'}
506 -
               ))),
507 -
             (MatchesListwise([
508 -
                 Equals(b'POST'),
509 -
                 Equals(u'https://example.org/acme/reg/1'),
510 -
                 Equals({}),
511 -
                 Always(),
512 -
                 on_jws(Equals({
513 -
                     u'resource': u'reg',
514 -
                     u'contact': [u'mailto:example@example.com']}))]),
515 -
              (http.ACCEPTED,
516 -
               {b'content-type': JSON_CONTENT_TYPE,
517 -
                b'replay-nonce': b64encode(b'Nonce3'),
518 -
                b'link': b','.join([
519 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
520 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
521 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
522 -
                ])},
523 -
               _json_dumps({
524 -
                   u'key': {
525 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
526 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
527 -
                       u'e': u'AQAB',
528 -
                       u'kty': u'RSA'},
529 -
                   u'contact': [u'mailto:example@example.com'],
530 -
                   u'agreement': u'https://example.org/acme/terms',
531 -
               })))],
532 -
            self.expectThat)
533 -
        client = self.useFixture(
534 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
535 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
536 -
        with sequence.consume(self.fail):
537 -
            d = client.register(reg)
538 -
            self.assertThat(
539 -
                d, succeeded(MatchesStructure(
540 -
                    body=MatchesStructure(
541 -
                        key=Equals(RSA_KEY_512.public_key()),
542 -
                        contact=Equals(reg.contact)),
543 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
544 -
                    new_authzr_uri=Equals(
545 -
                        u'https://example.org/acme/new-authz'),
546 -
                    terms_of_service=Equals(u'https://example.org/acme/terms'),
547 -
                )))
548 -
549 -
    def test_register_existing_update(self):
550 -
        """
551 -
        If registration fails due to our key already being registered, the
552 -
        existing registration is updated.
553 -
        """
554 -
        sequence = RequestSequence(
555 -
            [_nonce_response(
556 -
                u'https://example.org/acme/new-reg',
557 -
                b'Nonce'),
558 -
             (MatchesListwise([
559 -
                 Equals(b'POST'),
560 -
                 Equals(u'https://example.org/acme/new-reg'),
561 -
                 Equals({}),
562 -
                 Always(),
563 -
                 on_jws(Equals({
564 -
                     u'resource': u'new-reg',
565 -
                     u'contact': [u'mailto:example2@example.com']}))]),
566 -
              (http.CONFLICT,
567 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
568 -
                b'replay-nonce': b64encode(b'Nonce2'),
569 -
                b'location': b'https://example.org/acme/reg/1',
570 -
                },
571 -
               _json_dumps(
572 -
                   {u'status': http.CONFLICT,
573 -
                    u'type': u'urn:acme:error:malformed',
574 -
                    u'detail': u'Registration key is already in use'}
575 -
               ))),
576 -
             (MatchesListwise([
577 -
                 Equals(b'POST'),
578 -
                 Equals(u'https://example.org/acme/reg/1'),
579 -
                 Equals({}),
580 -
                 Always(),
581 -
                 on_jws(Equals({
582 -
                     u'resource': u'reg',
583 -
                     u'contact': [u'mailto:example2@example.com']}))]),
584 -
              (http.ACCEPTED,
585 -
               {b'content-type': JSON_CONTENT_TYPE,
586 -
                b'replay-nonce': b64encode(b'Nonce3'),
587 -
                b'link': b','.join([
588 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
589 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
590 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
591 -
                ])},
592 -
               _json_dumps({
593 -
                   u'key': {
594 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
595 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
596 -
                       u'e': u'AQAB',
597 -
                       u'kty': u'RSA'},
598 -
                   u'contact': [u'mailto:example2@example.com'],
599 -
                   u'agreement': u'https://example.org/acme/terms',
600 -
               })))],
601 -
            self.expectThat)
602 -
        client = self.useFixture(
603 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
604 -
        reg = messages.NewRegistration.from_data(email=u'example2@example.com')
605 -
        with sequence.consume(self.fail):
606 -
            d = client.register(reg)
607 -
            self.assertThat(
608 -
                d, succeeded(MatchesStructure(
609 -
                    body=MatchesStructure(
610 -
                        key=Equals(RSA_KEY_512.public_key()),
611 -
                        contact=Equals(reg.contact)),
612 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
613 -
                    new_authzr_uri=Equals(
614 -
                        u'https://example.org/acme/new-authz'),
615 -
                    terms_of_service=Equals(u'https://example.org/acme/terms'),
616 -
                )))
617 -
618 -
    def test_register_error(self):
619 -
        """
620 -
        If some other error occurs during registration, a
621 -
        :exc:`txacme.client.ServerError` results.
622 -
        """
623 -
        sequence = RequestSequence(
624 -
            [_nonce_response(
625 -
                u'https://example.org/acme/new-reg',
626 -
                b'Nonce'),
627 -
             (MatchesListwise([
628 -
                 Equals(b'POST'),
629 -
                 Equals(u'https://example.org/acme/new-reg'),
630 -
                 Equals({}),
631 -
                 Always(),
632 -
                 on_jws(Equals({
633 -
                     u'resource': u'new-reg',
634 -
                     u'contact': [u'mailto:example@example.com']}))]),
635 -
              (http.SERVICE_UNAVAILABLE,
636 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
637 -
                b'replay-nonce': b64encode(b'Nonce2'),
638 -
                },
639 -
               _json_dumps(
640 -
                   {u'status': http.SERVICE_UNAVAILABLE,
641 -
                    u'type': u'urn:acme:error:rateLimited',
642 -
                    u'detail': u'The request exceeds a rate limit'}
643 -
               )))],
644 -
            self.expectThat)
645 -
        client = self.useFixture(
646 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
647 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
648 -
        with sequence.consume(self.fail):
649 -
            d = client.register(reg)
650 -
            self.assertThat(
651 -
                d, failed_with(MatchesAll(
652 -
                    IsInstance(ServerError),
653 -
                    MatchesStructure(
654 -
                        message=MatchesStructure(
655 -
                            typ=Equals(u'urn:acme:error:rateLimited'),
656 -
                            detail=Equals(u'The request exceeds a rate limit'),
657 -
                            )))))
658 -
659 -
    def test_register_bad_nonce_once(self):
660 -
        """
661 -
        If a badNonce error is received, we clear all old nonces and retry the
662 -
        request once.
663 -
        """
664 -
        sequence = RequestSequence(
665 -
            [(MatchesListwise([
666 -
                 Equals(b'POST'),
667 -
                 Equals(u'https://example.org/acme/new-reg'),
668 -
                 Equals({}),
669 -
                 Always(),
670 -
                 on_jws(Equals({
671 -
                     u'resource': u'new-reg',
672 -
                     u'contact': [u'mailto:example@example.com']}))]),
673 -
              (http.SERVICE_UNAVAILABLE,
674 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
675 -
                b'replay-nonce': b64encode(b'Nonce2'),
676 -
                },
677 -
               _json_dumps(
678 -
                   {u'status': http.SERVICE_UNAVAILABLE,
679 -
                    u'type': u'urn:acme:error:badNonce',
680 -
                    u'detail': u'The client sent a bad nonce'}
681 -
               ))),
682 -
             (MatchesListwise([
683 -
                 Equals(b'POST'),
684 -
                 Equals(u'https://example.org/acme/new-reg'),
685 -
                 Equals({}),
686 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
687 -
                 on_jws(Equals({
688 -
                     u'resource': u'new-reg',
689 -
                     u'contact': [u'mailto:example@example.com'],
690 -
                 }), nonce=b'Nonce2')]),
691 -
              (http.CREATED,
692 -
               {b'content-type': JSON_CONTENT_TYPE,
693 -
                b'replay-nonce': b64encode(b'Nonce3'),
694 -
                b'location': b'https://example.org/acme/reg/1',
695 -
                b'link': b','.join([
696 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
697 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
698 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
699 -
                ])},
700 -
               _json_dumps({
701 -
                   u'key': {
702 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
703 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
704 -
                       u'e': u'AQAB',
705 -
                       u'kty': u'RSA'},
706 -
                   u'contact': [u'mailto:example@example.com'],
707 -
               })))],
708 -
            self.expectThat)
709 -
        client = self.useFixture(
710 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
711 -
        # Stash a few nonces so that we have some to clear on the retry.
712 -
        client._client._nonces.update(
713 -
            [b'OldNonce1', b'OldNonce2', b'OldNonce3', b'OldNonce4'])
714 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
715 -
        with sequence.consume(self.fail):
716 -
            d = client.register(reg)
717 -
            self.assertThat(
718 -
                d, succeeded(MatchesStructure(
719 -
                    body=MatchesStructure(
720 -
                        key=Equals(RSA_KEY_512.public_key()),
721 -
                        contact=Equals(reg.contact)),
722 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
723 -
                    new_authzr_uri=Equals(
724 -
                        u'https://example.org/acme/new-authz'),
725 -
                    terms_of_service=Equals(u'https://example.org/acme/terms'),
726 -
                )))
727 -
        self.assertThat(client._client._nonces, Equals(set([b'Nonce3'])))
728 -
729 -
    def test_register_bad_nonce_twice(self):
730 -
        """
731 -
        If a badNonce error is received on a retry, fail the request.
732 -
        """
733 -
        sequence = RequestSequence(
734 -
            [_nonce_response(
735 -
                u'https://example.org/acme/new-reg',
736 -
                b'Nonce'),
737 -
             (MatchesListwise([
738 -
                 Equals(b'POST'),
739 -
                 Equals(u'https://example.org/acme/new-reg'),
740 -
                 Equals({}),
741 -
                 Always(),
742 -
                 on_jws(Equals({
743 -
                     u'resource': u'new-reg',
744 -
                     u'contact': [u'mailto:example@example.com']}))]),
745 -
              (http.SERVICE_UNAVAILABLE,
746 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
747 -
                b'replay-nonce': b64encode(b'Nonce2'),
748 -
                },
749 -
               _json_dumps(
750 -
                   {u'status': http.SERVICE_UNAVAILABLE,
751 -
                    u'type': u'urn:acme:error:badNonce',
752 -
                    u'detail': u'The client sent a bad nonce'}
753 -
               ))),
754 -
             (MatchesListwise([
755 -
                 Equals(b'POST'),
756 -
                 Equals(u'https://example.org/acme/new-reg'),
757 -
                 Equals({}),
758 -
                 Always(),
759 -
                 on_jws(Equals({
760 -
                     u'resource': u'new-reg',
761 -
                     u'contact': [u'mailto:example@example.com']}))]),
762 -
              (http.SERVICE_UNAVAILABLE,
763 -
               {b'content-type': JSON_ERROR_CONTENT_TYPE,
764 -
                b'replay-nonce': b64encode(b'Nonce3'),
765 -
                },
766 -
               _json_dumps(
767 -
                   {u'status': http.SERVICE_UNAVAILABLE,
768 -
                    u'type': u'urn:acme:error:badNonce',
769 -
                    u'detail': u'The client sent a bad nonce'}
770 -
               )))],
771 -
            self.expectThat)
772 -
        client = self.useFixture(
773 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
774 -
        reg = messages.NewRegistration.from_data(email=u'example@example.com')
775 -
        with sequence.consume(self.fail):
776 -
            d = client.register(reg)
777 -
            self.assertThat(
778 -
                d, failed_with(MatchesAll(
779 -
                    IsInstance(ServerError),
780 -
                    MatchesStructure(
781 -
                        message=MatchesStructure(
782 -
                            typ=Equals(u'urn:acme:error:badNonce'),
783 -
                            detail=Equals(u'The client sent a bad nonce'),
784 -
                            )))))
785 -
786 -
    def test_agree_to_tos(self):
787 -
        """
788 -
        Agreeing to the TOS returns a registration with the agreement updated.
789 -
        """
790 -
        tos = u'https://example.org/acme/terms'
791 -
        sequence = RequestSequence(
792 -
            [_nonce_response(
793 -
                u'https://example.org/acme/reg/1',
794 -
                b'Nonce'),
795 -
             (MatchesListwise([
796 -
                 Equals(b'POST'),
797 -
                 Equals(u'https://example.org/acme/reg/1'),
798 -
                 Equals({}),
799 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
800 -
                 on_jws(ContainsDict({
801 -
                     u'resource': Equals(u'reg'),
802 -
                     u'agreement': Equals(tos)}))]),
803 -
              (http.ACCEPTED,
804 -
               {b'content-type': JSON_CONTENT_TYPE,
805 -
                b'replay-nonce': b64encode(b'Nonce2'),
806 -
                b'link': b','.join([
807 -
                    b'<https://example.org/acme/new-authz>;rel="next"',
808 -
                    b'<https://example.org/acme/recover-reg>;rel="recover"',
809 -
                    b'<https://example.org/acme/terms>;rel="terms-of-service"',
810 -
                ])},
811 -
               _json_dumps({
812 -
                   u'key': {
813 -
                       u'n': u'rlQR-WPFDjJn-vz3Y4HIseX3t0H9sqVEvPSL1gexDJkZDK6'
814 -
                             u'4AR3CLPg9kh2lXsMr0FysPuAspeHb75OVKFC1JQ',
815 -
                       u'e': u'AQAB',
816 -
                       u'kty': u'RSA'},
817 -
                   u'contact': [u'mailto:example@example.com'],
818 -
                   u'agreement': tos,
819 -
               })))],
820 -
            self.expectThat)
821 -
        client = self.useFixture(
822 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
823 -
        reg = messages.RegistrationResource(
824 -
            body=messages.Registration(
825 -
                contact=(u'mailto:example@example.com',),
826 -
                key=RSA_KEY_512.public_key()),
827 -
            uri=u'https://example.org/acme/reg/1',
828 -
            new_authzr_uri=u'https://example.org/acme/new-authz',
829 -
            terms_of_service=tos)
830 -
        with sequence.consume(self.fail):
831 -
            d = client.agree_to_tos(reg)
832 -
            self.assertThat(
833 -
                d, succeeded(MatchesStructure(
834 -
                    body=MatchesStructure(
835 -
                        key=Equals(RSA_KEY_512.public_key()),
836 -
                        contact=Equals(reg.body.contact),
837 -
                        agreement=Equals(tos)),
838 -
                    uri=Equals(u'https://example.org/acme/reg/1'),
839 -
                    new_authzr_uri=Equals(
840 -
                        u'https://example.org/acme/new-authz'),
841 -
                    terms_of_service=Equals(tos),
842 -
                )))
843 -
844 -
    def test_from_directory(self):
845 -
        """
846 -
        :func:`~txacme.client.Client.from_url` constructs a client with a
847 -
        directory retrieved from the given URL.
848 -
        """
849 -
        new_reg = u'https://example.org/acme/new-reg'
850 -
        sequence = RequestSequence(
851 -
            [(MatchesListwise([
852 -
                Equals(b'GET'),
853 -
                Equals(u'https://example.org/acme/'),
854 -
                Always(),
855 -
                Always(),
856 -
                Always()]),
857 -
             (http.OK,
858 -
              {b'content-type': JSON_CONTENT_TYPE,
859 -
               b'replay-nonce': b64encode(b'Nonce')},
860 -
              _json_dumps({
861 -
                  u'new-reg': new_reg,
862 -
                  u'revoke-cert': u'https://example.org/acme/revoke-cert',
863 -
                  u'new-authz': u'https://example.org/acme/new-authz',
864 -
              })))],
865 -
            self.expectThat)
866 -
        agent = RequestTraversalAgent(
867 -
            StringStubbingResource(sequence))
868 -
        jws_client = JWSClient(agent, key=RSA_KEY_512, alg=RS256)
869 -
        jws_client._treq._data_to_body_producer = _SynchronousProducer
870 -
        with sequence.consume(self.fail):
871 -
            d = Client.from_url(
872 -
                reactor, URL.fromText(u'https://example.org/acme/'),
873 -
                key=RSA_KEY_512, alg=RS256,
874 -
                jws_client=jws_client)
875 -
            self.assertThat(
876 -
                d,
877 -
                succeeded(
878 -
                    MatchesAll(
879 -
                        AfterPreprocessing(
880 -
                            lambda client:
881 -
                            client.directory[messages.NewRegistration()],
882 -
                            Equals(new_reg)))))
883 -
884 -
    def test_default_client(self):
885 -
        """
886 -
        ``~txacme.client._default_client`` constructs a client if one was not
887 -
        provided.
888 -
        """
889 -
        reactor = MemoryReactor()
890 -
        client = _default_client(None, reactor, RSA_KEY_512, RS384)
891 -
        self.assertThat(client, IsInstance(JWSClient))
892 -
        # We should probably assert some stuff about the treq.HTTPClient, but
893 -
        # it's hard without doing awful mock stuff.
894 -
895 -
    def test_request_challenges(self):
896 -
        """
897 -
        :meth:`~txacme.client.Client.request_challenges` creates a new
898 -
        authorization, and returns the authorization resource with a list of
899 -
        possible challenges to proceed with.
900 -
        """
901 -
        name = u'example.com'
902 -
        identifier_json = {u'type': u'dns',
903 -
                           u'value': name}
904 -
        identifier = messages.Identifier.from_json(identifier_json)
905 -
        challenges = [
906 -
            {u'type': u'http-01',
907 -
             u'uri': u'https://example.org/acme/authz/1/0',
908 -
             u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
909 -
            {u'type': u'dns',
910 -
             u'uri': u'https://example.org/acme/authz/1/1',
911 -
             u'token': u'DGyRejmCefe7v4NfDGDKfA'},
912 -
            ]
913 -
        sequence = RequestSequence(
914 -
            [_nonce_response(
915 -
                u'https://example.org/acme/new-authz',
916 -
                b'Nonce'),
917 -
             (MatchesListwise([
918 -
                 Equals(b'POST'),
919 -
                 Equals(u'https://example.org/acme/new-authz'),
920 -
                 Equals({}),
921 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
922 -
                 on_jws(Equals({
923 -
                     u'resource': u'new-authz',
924 -
                     u'identifier': identifier_json,
925 -
                     }))]),
926 -
              (http.CREATED,
927 -
               {b'content-type': JSON_CONTENT_TYPE,
928 -
                b'replay-nonce': b64encode(b'Nonce2'),
929 -
                b'location': b'https://example.org/acme/authz/1',
930 -
                b'link': b'<https://example.org/acme/new-cert>;rel="next"',
931 -
                },
932 -
               _json_dumps({
933 -
                   u'status': u'pending',
934 -
                   u'identifier': identifier_json,
935 -
                   u'challenges': challenges,
936 -
                   u'combinations': [[0], [1]],
937 -
               })))],
938 -
            self.expectThat)
939 -
        client = self.useFixture(
940 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
941 -
        with sequence.consume(self.fail):
942 -
            self.assertThat(
943 -
                client.request_challenges(identifier),
944 -
                succeeded(MatchesStructure(
945 -
                    body=MatchesStructure(
946 -
                        identifier=Equals(identifier),
947 -
                        challenges=Equals(
948 -
                            tuple(map(
949 -
                                messages.ChallengeBody.from_json,
950 -
                                challenges))),
951 -
                        combinations=Equals(((0,), (1,))),
952 -
                        status=Equals(messages.STATUS_PENDING)),
953 -
                    new_cert_uri=Equals(
954 -
                        u'https://example.org/acme/new-cert'),
955 -
                )))
956 -
957 -
    @example(http.CREATED, http.FOUND)
958 -
    @given(s.sampled_from(sorted(http.RESPONSES)),
959 -
           s.sampled_from(sorted(http.RESPONSES)))
960 -
    def test_expect_response_wrong_code(self, expected, actual):
961 -
        """
962 -
        ``_expect_response`` raises `~acme.errors.ClientError` if the response
963 -
        code does not match the expected code.
964 -
        """
965 -
        assume(expected != actual)
966 -
        response = TestResponse(code=actual)
967 -
        with ExpectedException(errors.ClientError):
968 -
            Client._expect_response(response, expected)
969 -
970 -
    def test_authorization_missing_link(self):
971 -
        """
972 -
        ``_parse_authorization`` raises `~acme.errors.ClientError` if the
973 -
        ``"next"`` link is missing.
974 -
        """
975 -
        response = TestResponse()
976 -
        with ExpectedException(errors.ClientError, '"next" link missing'):
977 -
            Client._parse_authorization(response)
978 -
979 -
    def test_authorization_unexpected_identifier(self):
980 -
        """
981 -
        ``_check_authorization`` raises `~acme.errors.UnexpectedUpdate` if the
982 -
        return identifier doesn't match.
983 -
        """
984 -
        with ExpectedException(errors.UnexpectedUpdate):
985 -
            Client._check_authorization(
986 -
                messages.AuthorizationResource(
987 -
                    body=messages.Authorization()),
988 -
                messages.Identifier(
989 -
                    typ=messages.IDENTIFIER_FQDN, value=u'example.org'))
990 -
991 -
    @example(u'example.com')
992 -
    @given(ts.dns_names())
993 -
    def test_fqdn_identifier(self, name):
189 +
    def test_fqdn_identifier(self):
994 190
        """
995 191
        `~txacme.client.fqdn_identifier` constructs an
996 192
        `~acme.messages.Identifier` of the right type.
997 193
        """
998 -
        self.assertThat(
999 -
            fqdn_identifier(name),
1000 -
            MatchesStructure(
1001 -
                typ=Equals(messages.IDENTIFIER_FQDN),
1002 -
                value=Equals(name)))
1003 -
1004 -
    def test_answer_challenge(self):
1005 -
        """
1006 -
        `~txacme.client.Client.answer_challenge` responds to a challenge and
1007 -
        returns the updated challenge.
1008 -
        """
1009 -
        key_authorization = u'blahblahblah'
1010 -
        uri = u'https://example.org/acme/authz/1/0'
1011 -
        sequence = RequestSequence(
1012 -
            [_nonce_response(
1013 -
                u'https://example.org/acme/authz/1/0',
1014 -
                b'Nonce'),
1015 -
             (MatchesListwise([
1016 -
                 Equals(b'POST'),
1017 -
                 Equals(uri),
1018 -
                 Equals({}),
1019 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
1020 -
                 on_jws(Equals({
1021 -
                     u'resource': u'challenge',
1022 -
                     u'type': u'http-01',
1023 -
                     }))]),
1024 -
              (http.OK,
1025 -
               {b'content-type': JSON_CONTENT_TYPE,
1026 -
                b'replay-nonce': b64encode(b'Nonce2'),
1027 -
                b'link': b'<https://example.org/acme/authz/1>;rel="up"',
1028 -
                },
1029 -
               _json_dumps({
1030 -
                   u'uri': uri,
1031 -
                   u'type': u'http-01',
1032 -
                   u'status': u'processing',
1033 -
                   u'token': u'DGyRejmCefe7v4NfDGDKfA',
1034 -
               })))],
1035 -
            self.expectThat)
1036 -
        client = self.useFixture(
1037 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1038 -
        with sequence.consume(self.fail):
1039 -
            self.assertThat(
1040 -
                client.answer_challenge(
1041 -
                    messages.ChallengeBody(
1042 -
                        uri=uri,
1043 -
                        chall=challenges.HTTP01(token=b'blahblah'),
1044 -
                        status=messages.STATUS_PENDING),
1045 -
                    challenges.HTTP01Response(
1046 -
                        key_authorization=key_authorization)),
1047 -
                succeeded(MatchesStructure(
1048 -
                    body=MatchesStructure(),
1049 -
                    authzr_uri=Equals(
1050 -
                        u'https://example.org/acme/authz/1'),
1051 -
                )))
1052 -
1053 -
    def test_challenge_missing_link(self):
1054 -
        """
1055 -
        ``_parse_challenge`` raises `~acme.errors.ClientError` if the ``"up"``
1056 -
        link is missing.
1057 -
        """
1058 -
        response = TestResponse()
1059 -
        with ExpectedException(errors.ClientError, '"up" link missing'):
1060 -
            Client._parse_challenge(response)
194 +
        name = u'example.com'
195 +
        result = fqdn_identifier(name)
196 +
        self.assertEqual(messages.IDENTIFIER_FQDN, result.typ)
197 +
        self.assertEqual(name, result.value)
1061 198
1062 -
    @example(URL.fromText(u'https://example.org/'),
1063 -
             URL.fromText(u'https://example.com/'))
1064 -
    @given(ts.urls(), ts.urls())
1065 -
    def test_challenge_unexpected_uri(self, url1, url2):
199 +
    def test_challenge_unexpected_uri(self):
1066 200
        """
1067 201
        ``_check_challenge`` raises `~acme.errors.UnexpectedUpdate` if the
1068 202
        challenge does not have the expected URI.
1069 203
        """
1070 -
        url1 = url1.asURI().asText()
1071 -
        url2 = url2.asURI().asText()
1072 -
        assume(url1 != url2)
1073 -
        with ExpectedException(errors.UnexpectedUpdate):
204 +
        # Crazy dance that was used in previous test.
205 +
        url1 = URL.fromText(u'https://example.org/').asURI().asText()
206 +
        url2 = URL.fromText(u'https://example.com/').asURI().asText()
207 +
208 +
        with self.assertRaises(errors.UnexpectedUpdate):
1074 209
            Client._check_challenge(
1075 -
                messages.ChallengeResource(
210 +
                challenge=messages.ChallengeResource(
1076 211
                    body=messages.ChallengeBody(chall=None, uri=url1)),
1077 -
                messages.ChallengeBody(chall=None, uri=url2))
1078 -
1079 -
    @example(now=1459184402., name=u'example.com', retry_after=60,
1080 -
             date_string=False)
1081 -
    @example(now=1459184402., name=u'example.org', retry_after=60,
1082 -
             date_string=True)
1083 -
    @given(now=s.floats(min_value=0., max_value=2147483648.),
1084 -
           name=ts.dns_names(),
1085 -
           retry_after=s.none() | s.integers(min_value=0, max_value=1000),
1086 -
           date_string=s.booleans())
1087 -
    def test_poll(self, now, name, retry_after, date_string):
1088 -
        """
1089 -
        `~txacme.client.Client.poll` retrieves the latest state of an
1090 -
        authorization resource, as well as the minimum time to wait before
1091 -
        polling the state again.
1092 -
        """
1093 -
        if retry_after is None:
1094 -
            retry_after_encoded = None
1095 -
            retry_after = 5
1096 -
        elif date_string:
1097 -
            retry_after_encoded = http.datetimeToString(retry_after + now)
1098 -
        else:
1099 -
            retry_after_encoded = u'{}'.format(retry_after).encode('ascii')
1100 -
        identifier_json = {u'type': u'dns',
1101 -
                           u'value': name}
1102 -
        identifier = messages.Identifier.from_json(identifier_json)
1103 -
        challenges = [
1104 -
            {u'type': u'http-01',
1105 -
             u'status': u'invalid',
1106 -
             u'uri': u'https://example.org/acme/authz/1/0',
1107 -
             u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
1108 -
            {u'type': u'dns',
1109 -
             u'status': u'pending',
1110 -
             u'uri': u'https://example.org/acme/authz/1/1',
1111 -
             u'token': u'DGyRejmCefe7v4NfDGDKfA'},
1112 -
            ]
1113 -
        authzr = messages.AuthorizationResource(
1114 -
            uri=u'https://example.org/acme/authz/1',
1115 -
            body=messages.Authorization(
1116 -
                identifier=identifier))
1117 -
        response_headers = {
1118 -
            b'content-type': JSON_CONTENT_TYPE,
1119 -
            b'replay-nonce': b64encode(b'Nonce2'),
1120 -
            b'location': b'https://example.org/acme/authz/1',
1121 -
            b'link': b'<https://example.org/acme/new-cert>;rel="next"',
1122 -
            }
1123 -
        if retry_after_encoded is not None:
1124 -
            response_headers[b'retry-after'] = retry_after_encoded
1125 -
        sequence = RequestSequence(
1126 -
            [(MatchesListwise([
1127 -
                Equals(b'GET'),
1128 -
                Equals(u'https://example.org/acme/authz/1'),
1129 -
                Equals({}),
1130 -
                Always(),
1131 -
                Always()]),
1132 -
              (http.OK,
1133 -
               response_headers,
1134 -
               _json_dumps({
1135 -
                   u'status': u'invalid',
1136 -
                   u'identifier': identifier_json,
1137 -
                   u'challenges': challenges,
1138 -
                   u'combinations': [[0], [1]],
1139 -
               })))],
1140 -
            self.expectThat)
1141 -
        fixture = self.useFixture(
1142 -
            ClientFixture(sequence, key=RSA_KEY_512))
1143 -
        fixture.clock.rightNow = now
1144 -
        client = fixture.client
1145 -
        with sequence.consume(self.fail):
1146 -
            self.assertThat(
1147 -
                client.poll(authzr),
1148 -
                succeeded(MatchesListwise([
1149 -
                    MatchesStructure(
1150 -
                        body=MatchesStructure(
1151 -
                            identifier=Equals(identifier),
1152 -
                            challenges=Equals(
1153 -
                                tuple(map(
1154 -
                                    messages.ChallengeBody.from_json,
1155 -
                                    challenges))),
1156 -
                            combinations=Equals(((0,), (1,))),
1157 -
                            status=Equals(messages.STATUS_INVALID)),
1158 -
                        new_cert_uri=Equals(
1159 -
                            u'https://example.org/acme/new-cert')),
1160 -
                    Nearly(retry_after, 1.0),
1161 -
                ])))
1162 -
1163 -
    def test_tls_sni_01_no_singleton(self):
1164 -
        """
1165 -
        If a suitable singleton challenge is not found,
1166 -
        `.NoSupportedChallenges` is raised.
1167 -
        """
1168 -
        challs = [
1169 -
            {u'type': u'http-01',
1170 -
             u'uri': u'https://example.org/acme/authz/1/0',
1171 -
             u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
1172 -
            {u'type': u'dns',
1173 -
             u'uri': u'https://example.org/acme/authz/1/1',
1174 -
             u'token': u'DGyRejmCefe7v4NfDGDKfA'},
1175 -
            {u'type': u'tls-sni-01',
1176 -
             u'uri': u'https://example.org/acme/authz/1/2',
1177 -
             u'token': u'f8IfXqddYr8IJqYHSH6NpA'},
1178 -
            ]
1179 -
        combinations = ((0, 2), (1, 2))
1180 -
        authzr = messages.AuthorizationResource(
1181 -
            body=messages.Authorization(
1182 -
                challenges=list(map(
1183 -
                    messages.ChallengeBody.from_json,
1184 -
                    challs)),
1185 -
                combinations=combinations))
1186 -
        with ExpectedException(NoSupportedChallenges):
1187 -
            _find_supported_challenge(
1188 -
                authzr, [NullResponder(challenges.TLSSNI01.typ)])
1189 -
1190 -
    def test_no_tls_sni_01(self):
1191 -
        """
1192 -
        If no tls-sni-01 challenges are available, `.NoSupportedChallenges` is
1193 -
        raised.
1194 -
        """
1195 -
        challs = [
1196 -
            {u'type': u'http-01',
1197 -
             u'uri': u'https://example.org/acme/authz/1/0',
1198 -
             u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
1199 -
            {u'type': u'dns',
1200 -
             u'uri': u'https://example.org/acme/authz/1/1',
1201 -
             u'token': u'DGyRejmCefe7v4NfDGDKfA'},
1202 -
            {u'type': u'tls-sni-01',
1203 -
             u'uri': u'https://example.org/acme/authz/1/2',
1204 -
             u'token': u'f8IfXqddYr8IJqYHSH6NpA'},
1205 -
            ]
1206 -
        combinations = ((0,), (1,))
1207 -
        authzr = messages.AuthorizationResource(
1208 -
            body=messages.Authorization(
1209 -
                challenges=list(map(
1210 -
                    messages.ChallengeBody.from_json,
1211 -
                    challs)),
1212 -
                combinations=combinations))
1213 -
        with ExpectedException(NoSupportedChallenges):
1214 -
            _find_supported_challenge(
1215 -
                authzr, [NullResponder(challenges.TLSSNI01.typ)])
1216 -
1217 -
    def test_only_tls_sni_01(self):
1218 -
        """
1219 -
        If a singleton tls-sni-01 challenge is available, it is returned.
1220 -
        """
1221 -
        challs = list(map(
1222 -
            messages.ChallengeBody.from_json,
1223 -
            [{u'type': u'http-01',
1224 -
              u'uri': u'https://example.org/acme/authz/1/0',
1225 -
              u'token': u'IlirfxKKXAsHtmzK29Pj8A'},
1226 -
             {u'type': u'dns',
1227 -
              u'uri': u'https://example.org/acme/authz/1/1',
1228 -
              u'token': u'DGyRejmCefe7v4NfDGDKfA'},
1229 -
             {u'type': u'tls-sni-01',
1230 -
              u'uri': u'https://example.org/acme/authz/1/2',
1231 -
              u'token': u'f8IfXqddYr8IJqYHSH6NpA'},
1232 -
             ]))
1233 -
        combinations = ((0,), (1,), (2,))
1234 -
        authzr = messages.AuthorizationResource(
1235 -
            body=messages.Authorization(
1236 -
                challenges=challs,
1237 -
                combinations=combinations))
1238 -
        responder = NullResponder(challenges.TLSSNI01.typ)
1239 -
        self.assertThat(
1240 -
            _find_supported_challenge(authzr, [responder]),
1241 -
            MatchesListwise([
1242 -
                Is(responder),
1243 -
                MatchesAll(
1244 -
                    IsInstance(messages.ChallengeBody),
1245 -
                    MatchesStructure(
1246 -
                        chall=IsInstance(challenges.TLSSNI01)))]))
1247 -
1248 -
    def test_answer_challenge_function(self):
1249 -
        """
1250 -
        The challenge is found in the responder after invoking
1251 -
        `~txacme.client.answer_challenge`.
1252 -
        """
1253 -
        recorded_challenges = set()
1254 -
        responder = RecordingResponder(recorded_challenges, u'tls-sni-01')
1255 -
        uri = u'https://example.org/acme/authz/1/1'
1256 -
        challb = messages.ChallengeBody.from_json({
1257 -
            u'uri': uri,
1258 -
            u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1259 -
            u'type': u'tls-sni-01',
1260 -
            u'status': u'pending'})
1261 -
        identifier_json = {u'type': u'dns',
1262 -
                           u'value': u'example.com'}
1263 -
        identifier = messages.Identifier.from_json(identifier_json)
1264 -
        authzr = messages.AuthorizationResource(
1265 -
            body=messages.Authorization(
1266 -
                identifier=identifier,
1267 -
                challenges=[challb],
1268 -
                combinations=[[0]]))
1269 -
        sequence = RequestSequence(
1270 -
            [_nonce_response(
1271 -
                u'https://example.org/acme/authz/1/1',
1272 -
                b'Nonce'),
1273 -
             (MatchesListwise([
1274 -
                 Equals(b'POST'),
1275 -
                 Equals(uri),
1276 -
                 Equals({}),
1277 -
                 ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
1278 -
                 on_jws(Equals({
1279 -
                     u'resource': u'challenge',
1280 -
                     u'type': u'tls-sni-01',
1281 -
                     }))]),
1282 -
              (http.OK,
1283 -
               {b'content-type': JSON_CONTENT_TYPE,
1284 -
                b'replay-nonce': b64encode(b'Nonce2'),
1285 -
                b'link': b'<https://example.org/acme/authz/1>;rel="up"',
1286 -
                },
1287 -
               _json_dumps({
1288 -
                   u'uri': uri,
1289 -
                   u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1290 -
                   u'type': u'tls-sni-01',
1291 -
                   u'status': u'processing',
1292 -
               })))],
1293 -
            self.expectThat)
1294 -
        client = self.useFixture(
1295 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1296 -
        with sequence.consume(self.fail):
1297 -
            d = answer_challenge(authzr, client, [responder])
1298 -
            self.assertThat(d, succeeded(Always()))
1299 -
            stop_responding = d.result
1300 -
            self.assertThat(
1301 -
                recorded_challenges,
1302 -
                MatchesListwise([
1303 -
                    IsInstance(challenges.TLSSNI01)
1304 -
                ]))
1305 -
            self.assertThat(
1306 -
                stop_responding(),
1307 -
                succeeded(Always()))
1308 -
            self.assertThat(recorded_challenges, Equals(set()))
1309 -
1310 -
    def _make_poll_response(self, uri, identifier_json):
1311 -
        """
1312 -
        Return a factory for a poll response.
1313 -
        """
1314 -
        def rr(status, error=None):
1315 -
            chall = {
1316 -
                u'type': u'tls-sni-01',
1317 -
                u'status': status,
1318 -
                u'uri': uri + u'/0',
1319 -
                u'token': u'IlirfxKKXAsHtmzK29Pj8A'}
1320 -
            if error is not None:
1321 -
                chall[u'error'] = error
1322 -
            return (
1323 -
                MatchesListwise([
1324 -
                    Equals(b'GET'),
1325 -
                    Equals(uri),
1326 -
                    Equals({}),
1327 -
                    Always(),
1328 -
                    Always()]),
1329 -
                (http.ACCEPTED,
1330 -
                 {b'content-type': JSON_CONTENT_TYPE,
1331 -
                  b'replay-nonce': b64encode(b'nonce2'),
1332 -
                  b'location': uri.encode('ascii'),
1333 -
                  b'link': b'<https://example.org/acme/new-cert>;rel="next"'},
1334 -
                 _json_dumps({
1335 -
                     u'status': status,
1336 -
                     u'identifier': identifier_json,
1337 -
                     u'challenges': [chall],
1338 -
                     u'combinations': [[0]],
1339 -
                 })))
1340 -
        return rr
1341 -
1342 -
    @example(name=u'example.com')
1343 -
    @given(name=ts.dns_names())
1344 -
    def test_poll_timeout(self, name):
1345 -
        """
1346 -
        If the timeout is exceeded during polling, `.poll_until_valid` will
1347 -
        fail with ``CancelledError``.
1348 -
        """
1349 -
        identifier_json = {u'type': u'dns', u'value': name}
1350 -
        uri = u'https://example.org/acme/authz/1'
1351 -
        rr = self._make_poll_response(uri, identifier_json)
1352 -
        sequence = RequestSequence(
1353 -
            [rr(u'pending'),
1354 -
             rr(u'pending'),
1355 -
             rr(u'pending'),
1356 -
             ], self.expectThat)
1357 -
        clock = Clock()
1358 -
        challb = messages.ChallengeBody.from_json({
1359 -
            u'uri': uri + u'/0',
1360 -
            u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1361 -
            u'type': u'tls-sni-01',
1362 -
            u'status': u'pending'})
1363 -
        authzr = messages.AuthorizationResource(
1364 -
            uri=uri,
1365 -
            body=messages.Authorization(
1366 -
                identifier=messages.Identifier.from_json(identifier_json),
1367 -
                challenges=[challb],
1368 -
                combinations=[[0]]))
1369 -
        client = self.useFixture(
1370 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1371 -
        with sequence.consume(self.fail):
1372 -
            d = poll_until_valid(authzr, clock, client, timeout=14.)
1373 -
            clock.pump([5, 5, 5])
1374 -
            self.assertThat(
1375 -
                d,
1376 -
                failed_with(IsInstance(CancelledError)))
1377 -
1378 -
    @example(name=u'example.com')
1379 -
    @given(name=ts.dns_names())
1380 -
    def test_poll_invalid(self, name):
1381 -
        """
1382 -
        If the authorization enters an invalid state while polling,
1383 -
        `.poll_until_valid` will fail with `.AuthorizationFailed`.
1384 -
        """
1385 -
        identifier_json = {u'type': u'dns', u'value': name}
1386 -
        uri = u'https://example.org/acme/authz/1'
1387 -
        rr = self._make_poll_response(uri, identifier_json)
1388 -
        sequence = RequestSequence(
1389 -
            [rr(u'pending'),
1390 -
             rr(u'invalid', {
1391 -
                 u'type': u'urn:acme:error:connection',
1392 -
                 u'detail': u'Failed to connect'}),
1393 -
             ], self.expectThat)
1394 -
        clock = Clock()
1395 -
        challb = messages.ChallengeBody.from_json({
1396 -
            u'uri': uri + u'/0',
1397 -
            u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1398 -
            u'type': u'tls-sni-01',
1399 -
            u'status': u'pending',
1400 -
            })
1401 -
        authzr = messages.AuthorizationResource(
1402 -
            uri=uri,
1403 -
            body=messages.Authorization(
1404 -
                identifier=messages.Identifier.from_json(identifier_json),
1405 -
                challenges=[challb],
1406 -
                combinations=[[0]]))
1407 -
        client = self.useFixture(
1408 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1409 -
        with sequence.consume(self.fail):
1410 -
            d = poll_until_valid(authzr, clock, client, timeout=14.)
1411 -
            clock.pump([5, 5])
1412 -
            self.assertThat(
1413 -
                d,
1414 -
                failed_with(MatchesAll(
1415 -
                    IsInstance(AuthorizationFailed),
1416 -
                    MatchesStructure(
1417 -
                        status=Equals(messages.STATUS_INVALID),
1418 -
                        errors=Equals([
1419 -
                            messages.Error(
1420 -
                                typ=u'urn:acme:error:connection',
1421 -
                                detail=u'Failed to connect',
1422 -
                                title=None)])),
1423 -
                    AfterPreprocessing(
1424 -
                        repr,
1425 -
                        StartsWith(u'AuthorizationFailed(<Status(invalid)')))))
1426 -
1427 -
    @example(name=u'example.com')
1428 -
    @given(name=ts.dns_names())
1429 -
    def test_poll_valid(self, name):
1430 -
        """
1431 -
        If the authorization enters a valid state while polling,
1432 -
        `.poll_until_valid` will fire with the updated authorization.
1433 -
        """
1434 -
        identifier_json = {u'type': u'dns', u'value': name}
1435 -
        uri = u'https://example.org/acme/authz/1'
1436 -
        rr = self._make_poll_response(uri, identifier_json)
1437 -
        sequence = RequestSequence(
1438 -
            [rr(u'pending'),
1439 -
             rr(u'valid'),
1440 -
             ], self.expectThat)
1441 -
        clock = Clock()
1442 -
        challb = messages.ChallengeBody.from_json({
1443 -
            u'uri': uri + u'/0',
1444 -
            u'token': u'IlirfxKKXAsHtmzK29Pj8A',
1445 -
            u'type': u'tls-sni-01',
1446 -
            u'status': u'pending',
1447 -
            })
1448 -
        authzr = messages.AuthorizationResource(
1449 -
            uri=uri,
1450 -
            body=messages.Authorization(
1451 -
                identifier=messages.Identifier.from_json(identifier_json),
1452 -
                challenges=[challb],
1453 -
                combinations=[[0]]))
1454 -
        client = self.useFixture(
1455 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1456 -
        with sequence.consume(self.fail):
1457 -
            d = poll_until_valid(authzr, clock, client, timeout=14.)
1458 -
            clock.pump([5, 5])
1459 -
            self.assertThat(
1460 -
                d,
1461 -
                succeeded(IsInstance(messages.AuthorizationResource)))
1462 -
1463 -
    @example(name=u'example.com',
1464 -
             issuer_url=URL.fromText(u'https://example.org/acme/ca-cert'))
1465 -
    @given(name=ts.dns_names(),
1466 -
           issuer_url=ts.urls())
1467 -
    def test_request_issuance(self, name, issuer_url):
1468 -
        """
1469 -
        If issuing is successful, a certificate resource is returned.
1470 -
        """
1471 -
        assume(len(name) <= 64)
1472 -
        cert_request = CertificateRequest(
1473 -
            csr=csr_for_names([name], RSA_KEY_512_RAW))
1474 -
        cert, _ = generate_tls_sni_01_cert(
1475 -
            name, _generate_private_key=lambda _: RSA_KEY_512_RAW)
1476 -
        cert_bytes = cert.public_bytes(serialization.Encoding.DER)
1477 -
        sequence = RequestSequence([
1478 -
            _nonce_response(u'https://example.org/acme/new-cert', b'nonce'),
1479 -
            (MatchesListwise([
1480 -
                Equals(b'POST'),
1481 -
                Equals(u'https://example.org/acme/new-cert'),
1482 -
                Equals({}),
1483 -
                ContainsDict({b'Content-Type': Equals([JSON_CONTENT_TYPE])}),
1484 -
                on_jws(AfterPreprocessing(
1485 -
                    CertificateRequest.from_json,
1486 -
                    Equals(cert_request)))]),
1487 -
             (http.CREATED,
1488 -
              {b'content-type': DER_CONTENT_TYPE,
1489 -
               b'replay-nonce': b64encode(b'nonce2'),
1490 -
               b'location': b'https://example.org/acme/cert/asdf',
1491 -
               b'link': u'<{!s}>;rel="up"'.format(
1492 -
                   issuer_url.asURI().asText()).encode('utf-8')},
1493 -
              cert_bytes)),
1494 -
        ], self.expectThat)
1495 -
        client = self.useFixture(
1496 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1497 -
        with sequence.consume(self.fail):
1498 -
            self.assertThat(
1499 -
                client.request_issuance(
1500 -
                    CertificateRequest(
1501 -
                        csr=csr_for_names([name], RSA_KEY_512_RAW))),
1502 -
                succeeded(MatchesStructure(
1503 -
                    body=Equals(cert_bytes))))
1504 -
1505 -
    def test_fetch_chain_empty(self):
1506 -
        """
1507 -
        If a certificate has no issuer link, `.Client.fetch_chain` returns an
1508 -
        empty chain.
1509 -
        """
1510 -
        cert = messages.CertificateResource(cert_chain_uri=None)
1511 -
        sequence = RequestSequence([], self.expectThat)
1512 -
        client = self.useFixture(
1513 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1514 -
        with sequence.consume(self.fail):
1515 -
            self.assertThat(
1516 -
                client.fetch_chain(cert),
1517 -
                succeeded(Equals([])))
1518 -
1519 -
    def _make_cert_sequence(self, cert_urls):
1520 -
        """
1521 -
        Build a sequence for fetching a list of certificates.
1522 -
        """
1523 -
        return RequestSequence([
1524 -
            (MatchesListwise([
1525 -
                Equals(b'GET'),
1526 -
                Equals(url),
1527 -
                Equals({}),
1528 -
                ContainsDict({b'Accept': Equals([DER_CONTENT_TYPE])}),
1529 -
                Always()]),
1530 -
             (http.OK,
1531 -
              {b'content-type': DER_CONTENT_TYPE,
1532 -
               b'location': url.encode('utf-8'),
1533 -
               b'link':
1534 -
               u'<{!s}>;rel="up"'.format(
1535 -
                   issuer_url).encode('utf-8')
1536 -
               if issuer_url is not None else b''},
1537 -
              b''))
1538 -
            for url, issuer_url
1539 -
            in cert_urls
1540 -
            ], self.expectThat)
1541 -
1542 -
    @settings(deadline=None)
1543 -
    @example([u'http://example.com/1', u'http://example.com/2'])
1544 -
    @given(s.lists(s.integers()
1545 -
                   .map(lambda n: u'http://example.com/{}'.format(n)),
1546 -
                   min_size=1, max_size=10))
1547 -
    def test_fetch_chain_okay(self, cert_urls):
1548 -
        """
1549 -
        A certificate chain that is shorter than the max length is returned.
1550 -
        """
1551 -
        cert = messages.CertificateResource(
1552 -
            uri=u'http://example.com/',
1553 -
            cert_chain_uri=cert_urls[0])
1554 -
        urls = list(zip(cert_urls, cert_urls[1:] + [None]))
1555 -
        sequence = self._make_cert_sequence(urls)
1556 -
        client = self.useFixture(
1557 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1558 -
        with sequence.consume(self.fail):
1559 -
            self.assertThat(
1560 -
                client.fetch_chain(cert),
1561 -
                succeeded(
1562 -
                    MatchesListwise([
1563 -
                        MatchesStructure(
1564 -
                            uri=Equals(url),
1565 -
                            cert_chain_uri=Equals(issuer_url))
1566 -
                        for url, issuer_url in urls])))
1567 -
1568 -
    @settings(deadline=None)
1569 -
    @example([u'http://example.com/{}'.format(n) for n in range(20)])
1570 -
    @given(s.lists(s.integers()
1571 -
                   .map(lambda n: u'http://example.com/{}'.format(n)),
1572 -
                   min_size=11))
1573 -
    def test_fetch_chain_too_long(self, cert_urls):
1574 -
        """
1575 -
        A certificate chain that is too long fails with
1576 -
        `~acme.errors.ClientError`.
1577 -
        """
1578 -
        cert = messages.CertificateResource(
1579 -
            uri=u'http://example.com/',
1580 -
            cert_chain_uri=cert_urls[0])
1581 -
        sequence = self._make_cert_sequence(
1582 -
            list(zip(cert_urls, cert_urls[1:]))[:10])
1583 -
        client = self.useFixture(
1584 -
            ClientFixture(sequence, key=RSA_KEY_512)).client
1585 -
        with sequence.consume(self.fail):
1586 -
            self.assertThat(
1587 -
                client.fetch_chain(cert),
1588 -
                failed_with(IsInstance(errors.ClientError)))
212 +
                challenge_body=messages.ChallengeBody(chall=None, uri=url2),
213 +
                )
1589 214
1590 215
1591 216
class JWSClientTests(TestCase):
1592 217
    """
1593 218
    :class:`.JWSClient` implements JWS-signed requests over HTTP.
1594 219
    """
1595 -
    def test_check_invalid_json(self):
1596 -
        """
1597 -
        If a JSON response is expected, but a response is received with a
1598 -
        non-JSON Content-Type, :exc:`~acme.errors.ClientError` is raised.
1599 -
        """
1600 -
        self.assertThat(
1601 -
            JWSClient._check_response(
1602 -
                TestResponse(content_type=b'application/octet-stream')),
1603 -
            failed_with(IsInstance(errors.ClientError)))
1604 -
1605 -
    def test_check_invalid_error_type(self):
1606 -
        """
1607 -
        If an error response is received with a non-JSON-problem Content-Type,
1608 -
        :exc:`~acme.errors.ClientError` is raised.
1609 -
        """
1610 -
        self.assertThat(
1611 -
            JWSClient._check_response(
1612 -
                TestResponse(
1613 -
                    code=http.FORBIDDEN,
1614 -
                    content_type=b'application/octet-stream')),
1615 -
            failed_with(IsInstance(errors.ClientError)))
1616 -
220 +
    @defer.inlineCallbacks
1617 221
    def test_check_invalid_error(self):
1618 222
        """
1619 223
        If an error response is received but cannot be parsed,
1620 224
        :exc:`~acme.errors.ServerError` is raised.
1621 225
        """
1622 -
        self.assertThat(
1623 -
            JWSClient._check_response(
1624 -
                TestResponse(
1625 -
                    code=http.FORBIDDEN,
1626 -
                    content_type=JSON_ERROR_CONTENT_TYPE)),
1627 -
            failed_with(IsInstance(ServerError)))
226 +
        response = TestResponse(
227 +
            code=http.FORBIDDEN,
228 +
            content_type=JSON_ERROR_CONTENT_TYPE)
229 +
230 +
        with self.assertRaises(ServerError):
231 +
            yield JWSClient._check_response(response)
1628 232
233 +
    @defer.inlineCallbacks
1629 234
    def test_check_valid_error(self):
1630 235
        """
1631 236
        If an error response is received but cannot be parsed,
1632 237
        :exc:`~acme.errors.ClientError` is raised.
1633 238
        """
1634 -
        self.assertThat(
1635 -
            JWSClient._check_response(
1636 -
                TestResponse(
1637 -
                    code=http.FORBIDDEN,
1638 -
                    content_type=JSON_ERROR_CONTENT_TYPE,
1639 -
                    json=lambda: succeed({
1640 -
                        u'type': u'unauthorized',
1641 -
                        u'detail': u'blah blah blah'}))),
1642 -
            failed_with(
1643 -
                MatchesAll(
1644 -
                    IsInstance(ServerError),
1645 -
                    AfterPreprocessing(repr, StartsWith('ServerError')))))
1646 -
1647 -
    def test_check_expected_bad_json(self):
1648 -
        """
1649 -
        If a JSON response was expected, but could not be parsed,
1650 -
        :exc:`~acme.errors.ClientError` is raised.
1651 -
        """
1652 -
        self.assertThat(
1653 -
            JWSClient._check_response(
1654 -
                TestResponse(json=lambda: fail(ValueError()))),
1655 -
            failed_with(IsInstance(errors.ClientError)))
1656 -
1657 -
    def test_missing_nonce(self):
1658 -
        """
1659 -
        If the response from the server does not have a nonce,
1660 -
        :exc:`~acme.errors.MissingNonce` is raised.
1661 -
        """
1662 -
        client = JWSClient(None, None, None)
1663 -
        with ExpectedException(errors.MissingNonce):
1664 -
            client._add_nonce(TestResponse())
1665 -
1666 -
    def test_bad_nonce(self):
1667 -
        """
1668 -
        If the response from the server has an unparseable nonce,
1669 -
        :exc:`~acme.errors.BadNonce` is raised.
1670 -
        """
1671 -
        client = JWSClient(None, None, None)
1672 -
        with ExpectedException(errors.BadNonce):
1673 -
            client._add_nonce(TestResponse(nonce=b'a!_'))
1674 -
1675 -
    def test_already_nonce(self):
1676 -
        """
1677 -
        No request is made if we already have a nonce.
1678 -
        """
1679 -
        client = JWSClient(None, None, None)
1680 -
        client._nonces.add(u'nonce')
1681 -
        self.assertThat(client._get_nonce(b''), succeeded(Equals(u'nonce')))
1682 -
1683 -
1684 -
class ExtraCoverageTests(TestCase):
1685 -
    """
1686 -
    Tests to get coverage on some test helpers that we don't really want to
1687 -
    maintain ourselves.
1688 -
    """
1689 -
    def test_always_never(self):
1690 -
        self.assertThat(Always(), AfterPreprocessing(str, Equals('Always()')))
1691 -
        self.assertThat(Never(), AfterPreprocessing(str, Equals('Never()')))
1692 -
        self.assertThat(None, Not(Never()))
1693 -
        self.assertThat(
1694 -
            Nearly(1.0, 2.0),
1695 -
            AfterPreprocessing(str, Equals('Nearly(1.0, 2.0)')))
1696 -
        self.assertThat(2.0, Not(Nearly(1.0)))
1697 -
1698 -
    def test_unexpected_number_of_request_causes_failure(self):
1699 -
        """
1700 -
        If there are no more expected requests, making a request causes a
1701 -
        failure.
1702 -
        """
1703 -
        async_failures = []
1704 -
        sequence = RequestSequence(
1705 -
            [],
1706 -
            async_failure_reporter=lambda *a: async_failures.append(a))
1707 -
        client = HTTPClient(
1708 -
            agent=RequestTraversalAgent(
1709 -
                StringStubbingResource(sequence)),
1710 -
            data_to_body_producer=_SynchronousProducer)
1711 -
        d = client.get('https://anything', data=b'what', headers={b'1': b'1'})
1712 -
        self.assertThat(
1713 -
            d,
1714 -
            succeeded(MatchesStructure(code=Equals(500))))
1715 -
        self.assertEqual(1, len(async_failures))
1716 -
        self.assertIn("No more requests expected, but request",
1717 -
                      async_failures[0][2])
1718 -
1719 -
        # the expected requests have all been made
1720 -
        self.assertTrue(sequence.consumed())
1721 -
1722 -
    def test_consume_context_manager_fails_on_remaining_requests(self):
1723 -
        """
1724 -
        If the ``consume`` context manager is used, if there are any remaining
1725 -
        expecting requests, the test case will be failed.
1726 -
        """
1727 -
        sequence = RequestSequence(
1728 -
            [(Always(), (418, {}, b'body'))] * 2,
1729 -
            async_failure_reporter=self.assertThat)
1730 -
        client = HTTPClient(
1731 -
            agent=RequestTraversalAgent(
1732 -
                StringStubbingResource(sequence)),
1733 -
            data_to_body_producer=_SynchronousProducer)
1734 -
1735 -
        consume_failures = []
1736 -
        with sequence.consume(sync_failure_reporter=consume_failures.append):
1737 -
            self.assertThat(
1738 -
                client.get('https://anything', data=b'what',
1739 -
                           headers={b'1': b'1'}),
1740 -
                succeeded(Always()))
239 +
        response = TestResponse(
240 +
            code=http.FORBIDDEN,
241 +
            content_type=JSON_ERROR_CONTENT_TYPE,
242 +
            json=lambda: succeed({
243 +
                u'type': u'unauthorized',
244 +
                u'detail': u'blah blah blah'}))
1741 245
1742 -
        self.assertEqual(1, len(consume_failures))
1743 -
        self.assertIn(
1744 -
            "Not all expected requests were made.  Still expecting:",
1745 -
            consume_failures[0])
246 +
        with self.assertRaises(ServerError):
247 +
            yield JWSClient._check_response(response)
1746 248
1747 249
1748 250
class LinkParsingTests(TestCase):
@@ -1757,18 +259,18 @@
Loading
1757 259
        """
1758 260
        The first example from the RFC.
1759 261
        """
1760 -
        self.assertThat(
1761 -
            _parse_header_links(
1762 -
                TestResponse(
1763 -
                    links=[b'<http://example.com/TheBook/chapter2>; '
1764 -
                           b'rel="previous"; '
1765 -
                           b'title="previous chapter"'])),
1766 -
            Equals({
1767 -
                u'previous':
1768 -
                {u'rel': u'previous',
1769 -
                 u'title': u'previous chapter',
1770 -
                 u'url': u'http://example.com/TheBook/chapter2'}
1771 -
            }))
262 +
        response = TestResponse(links=[
263 +
            b'<http://example.com/TheBook/chapter2>; '
264 +
           b'rel="previous"; '
265 +
           b'title="previous chapter"'])
266 +
        result = _parse_header_links(response)
267 +
        self.assertEqual({
268 +
            u'previous':
269 +
            {u'rel': u'previous',
270 +
             u'title': u'previous chapter',
271 +
             u'url': u'http://example.com/TheBook/chapter2'}
272 +
            },
273 +
            result)
1772 274
1773 275
1774 276
__all__ = ['ClientTests', 'ExtraCoverageTests', 'LinkParsingTests']

@@ -2,11 +2,11 @@
Loading
2 2
3 3
4 4
LETSENCRYPT_DIRECTORY = URL.fromText(
5 -
    u'https://acme-v01.api.letsencrypt.org/directory')
5 +
    u'https://acme-v02.api.letsencrypt.org/directory')
6 6
7 7
8 8
LETSENCRYPT_STAGING_DIRECTORY = URL.fromText(
9 -
    u'https://acme-staging.api.letsencrypt.org/directory')
9 +
    u'https://acme-staging-v02.api.letsencrypt.org/directory')
10 10
11 11
12 12
__all__ = ['LETSENCRYPT_DIRECTORY', 'LETSENCRYPT_STAGING_DIRECTORY']

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Learn more Showing 7 files with coverage changes found.

Changes in src/txacme/util.py
-20
+1
+18
Loading file...
Changes in src/txacme/urls.py
-1
+1
Loading file...
Changes in src/txacme/client.py
-53
+1
+52
Loading file...
Changes in src/txacme/service.py
-17
+17
Loading file...
Changes in src/txacme/testing.py
-11
+1
+10
Loading file...
Changes in src/txacme/test/test_client.py
-29
+1
+27
Loading file...
Changes in src/txacme/logging.py
-1
+1
Loading file...

30 Commits

Hiding 28 contexual commits
-10 Files
-1136
-1499
+7
+356
Files Coverage
src/txacme -43.64% 49.17%
Project Totals (17 files) 49.17%
Loading