001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2017 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j;
015
016import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
017import static org.assertj.core.api.Assertions.*;
018import static org.junit.jupiter.api.Assertions.assertThrows;
019import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;
020import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
021import static org.shredzone.acme4j.toolbox.TestUtils.url;
022
023import java.io.IOException;
024import java.net.HttpURLConnection;
025import java.net.InetAddress;
026import java.net.URL;
027import java.time.Duration;
028import java.util.Arrays;
029
030import org.assertj.core.api.AutoCloseableSoftAssertions;
031import org.junit.jupiter.api.Test;
032import org.shredzone.acme4j.connector.Resource;
033import org.shredzone.acme4j.exception.AcmeNotSupportedException;
034import org.shredzone.acme4j.provider.TestableConnectionProvider;
035import org.shredzone.acme4j.toolbox.JSON;
036import org.shredzone.acme4j.toolbox.JSONBuilder;
037import org.shredzone.acme4j.toolbox.TestUtils;
038
039/**
040 * Unit tests for {@link OrderBuilder}.
041 */
042public class OrderBuilderTest {
043
044    private final URL resourceUrl  = url("http://example.com/acme/resource");
045    private final URL locationUrl  = url(TestUtils.ACCOUNT_URL);
046
047    /**
048     * Test that a new {@link Order} can be created.
049     */
050    @Test
051    public void testOrderCertificate() throws Exception {
052        var notBefore = parseTimestamp("2016-01-01T00:00:00Z");
053        var notAfter = parseTimestamp("2016-01-08T00:00:00Z");
054
055        var provider = new TestableConnectionProvider() {
056            @Override
057            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
058                assertThat(url).isEqualTo(resourceUrl);
059                assertThatJson(claims.toString()).isEqualTo(getJSON("requestOrderRequest").toString());
060                assertThat(login).isNotNull();
061                return HttpURLConnection.HTTP_CREATED;
062            }
063
064            @Override
065            public JSON readJsonResponse() {
066                return getJSON("requestOrderResponse");
067            }
068
069            @Override
070            public URL getLocation() {
071                return locationUrl;
072            }
073        };
074
075        var login = provider.createLogin();
076
077        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
078
079        var account = new Account(login);
080        var order = account.newOrder()
081                        .domains("example.com", "www.example.com")
082                        .domain("example.org")
083                        .domains(Arrays.asList("m.example.com", "m.example.org"))
084                        .identifier(Identifier.dns("d.example.com"))
085                        .identifiers(Arrays.asList(
086                                    Identifier.dns("d2.example.com"),
087                                    Identifier.ip(InetAddress.getByName("192.0.2.2"))))
088                        .notBefore(notBefore)
089                        .notAfter(notAfter)
090                        .create();
091
092        try (var softly = new AutoCloseableSoftAssertions()) {
093            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(
094                        Identifier.dns("example.com"),
095                        Identifier.dns("www.example.com"),
096                        Identifier.dns("example.org"),
097                        Identifier.dns("m.example.com"),
098                        Identifier.dns("m.example.org"),
099                        Identifier.dns("d.example.com"),
100                        Identifier.dns("d2.example.com"),
101                        Identifier.ip(InetAddress.getByName("192.0.2.2")));
102            softly.assertThat(order.getNotBefore().orElseThrow())
103                    .isEqualTo("2016-01-01T00:10:00Z");
104            softly.assertThat(order.getNotAfter().orElseThrow())
105                    .isEqualTo("2016-01-08T00:10:00Z");
106            softly.assertThat(order.getExpires().orElseThrow())
107                    .isEqualTo("2016-01-10T00:00:00Z");
108            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);
109            softly.assertThat(order.isAutoRenewing()).isFalse();
110            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
111                    .isThrownBy(order::getAutoRenewalStartDate);
112            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
113                    .isThrownBy(order::getAutoRenewalEndDate);
114            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
115                    .isThrownBy(order::getAutoRenewalLifetime);
116            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
117                    .isThrownBy(order::getAutoRenewalLifetimeAdjust);
118            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
119                    .isThrownBy(order::isAutoRenewalGetEnabled);
120            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
121                    .isThrownBy(order::getProfile);
122            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);
123            softly.assertThat(order.getAuthorizations()).isNotNull();
124            softly.assertThat(order.getAuthorizations()).hasSize(2);
125        }
126
127        provider.close();
128    }
129
130    /**
131     * Test that a new auto-renewal {@link Order} can be created.
132     */
133    @Test
134    public void testAutoRenewOrderCertificate() throws Exception {
135        var autoRenewStart = parseTimestamp("2018-01-01T00:00:00Z");
136        var autoRenewEnd = parseTimestamp("2019-01-01T00:00:00Z");
137        var validity = Duration.ofDays(7);
138        var predate = Duration.ofDays(6);
139
140        var provider = new TestableConnectionProvider() {
141            @Override
142            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
143                assertThat(url).isEqualTo(resourceUrl);
144                assertThatJson(claims.toString()).isEqualTo(getJSON("requestAutoRenewOrderRequest").toString());
145                assertThat(login).isNotNull();
146                return HttpURLConnection.HTTP_CREATED;
147            }
148
149            @Override
150            public JSON readJsonResponse() {
151                return getJSON("requestAutoRenewOrderResponse");
152            }
153
154            @Override
155            public URL getLocation() {
156                return locationUrl;
157            }
158        };
159
160        var login = provider.createLogin();
161
162        provider.putMetadata("auto-renewal",JSON.parse(
163                "{\"allow-certificate-get\": true}"
164        ).toMap());
165        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
166
167        var account = new Account(login);
168        var order = account.newOrder()
169                        .domain("example.org")
170                        .autoRenewal()
171                        .autoRenewalStart(autoRenewStart)
172                        .autoRenewalEnd(autoRenewEnd)
173                        .autoRenewalLifetime(validity)
174                        .autoRenewalLifetimeAdjust(predate)
175                        .autoRenewalEnableGet()
176                        .create();
177
178        try (var softly = new AutoCloseableSoftAssertions()) {
179            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(Identifier.dns("example.org"));
180            softly.assertThat(order.getNotBefore()).isEmpty();
181            softly.assertThat(order.getNotAfter()).isEmpty();
182            softly.assertThat(order.isAutoRenewing()).isTrue();
183            softly.assertThat(order.getAutoRenewalStartDate().orElseThrow()).isEqualTo(autoRenewStart);
184            softly.assertThat(order.getAutoRenewalEndDate()).isEqualTo(autoRenewEnd);
185            softly.assertThat(order.getAutoRenewalLifetime()).isEqualTo(validity);
186            softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow()).isEqualTo(predate);
187            softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue();
188            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);
189        }
190
191        provider.close();
192    }
193
194    /**
195     * Test that a new {@link Order} with ancestor domain can be created.
196     */
197    @Test
198    public void testOrderCertificateWithAncestor() throws Exception {
199        var notBefore = parseTimestamp("2016-01-01T00:00:00Z");
200        var notAfter = parseTimestamp("2016-01-08T00:00:00Z");
201
202        var provider = new TestableConnectionProvider() {
203            @Override
204            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
205                assertThat(url).isEqualTo(resourceUrl);
206                assertThatJson(claims.toString()).isEqualTo(getJSON("requestOrderRequestSub").toString());
207                assertThat(login).isNotNull();
208                return HttpURLConnection.HTTP_CREATED;
209            }
210
211            @Override
212            public JSON readJsonResponse() {
213                return getJSON("requestOrderResponseSub");
214            }
215
216            @Override
217            public URL getLocation() {
218                return locationUrl;
219            }
220        };
221
222        var login = provider.createLogin();
223
224        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
225        provider.putMetadata("subdomainAuthAllowed", true);
226
227        var account = new Account(login);
228        var order = account.newOrder()
229                .identifier(Identifier.dns("foo.bar.example.com").withAncestorDomain("example.com"))
230                .notBefore(notBefore)
231                .notAfter(notAfter)
232                .create();
233
234        try (var softly = new AutoCloseableSoftAssertions()) {
235            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(
236                    Identifier.dns("foo.bar.example.com"));
237            softly.assertThat(order.getNotBefore().orElseThrow())
238                    .isEqualTo("2016-01-01T00:10:00Z");
239            softly.assertThat(order.getNotAfter().orElseThrow())
240                    .isEqualTo("2016-01-08T00:10:00Z");
241            softly.assertThat(order.getExpires().orElseThrow())
242                    .isEqualTo("2016-01-10T00:00:00Z");
243            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);
244            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);
245            softly.assertThat(order.getAuthorizations()).isNotNull();
246            softly.assertThat(order.getAuthorizations()).hasSize(2);
247        }
248
249        provider.close();
250    }
251
252    /**
253     * Test that a new {@link Order} with ancestor domain fails if not supported.
254     */
255    @Test
256    public void testOrderCertificateWithAncestorFails() throws Exception {
257        var provider = new TestableConnectionProvider();
258
259        var login = provider.createLogin();
260
261        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
262
263        assertThat(login.getSession().getMetadata().isSubdomainAuthAllowed()).isFalse();
264
265        var account = new Account(login);
266        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() ->
267                account.newOrder()
268                        .identifier(Identifier.dns("foo.bar.example.com").withAncestorDomain("example.com"))
269                        .create()
270        );
271
272        provider.close();
273    }
274
275    /**
276     * Test that an auto-renewal {@link Order} cannot be created if unsupported by the CA.
277     */
278    @Test
279    public void testAutoRenewOrderCertificateFails() {
280        assertThrows(AcmeNotSupportedException.class, () -> {
281            var provider = new TestableConnectionProvider();
282            provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
283
284            var login = provider.createLogin();
285
286            var account = new Account(login);
287            account.newOrder()
288                            .domain("example.org")
289                            .autoRenewal()
290                            .create();
291
292            provider.close();
293        });
294    }
295
296    /**
297     * Test that auto-renew and notBefore/notAfter cannot be mixed.
298     */
299    @Test
300    public void testAutoRenewNotMixed() throws Exception {
301        var someInstant = parseTimestamp("2018-01-01T00:00:00Z");
302
303        var provider = new TestableConnectionProvider();
304        var login = provider.createLogin();
305
306        var account = new Account(login);
307
308        assertThrows(IllegalArgumentException.class, () -> {
309            OrderBuilder ob = account.newOrder().autoRenewal();
310            ob.notBefore(someInstant);
311        }, "accepted notBefore");
312
313        assertThrows(IllegalArgumentException.class, () -> {
314            OrderBuilder ob = account.newOrder().autoRenewal();
315            ob.notAfter(someInstant);
316        }, "accepted notAfter");
317
318        assertThrows(IllegalArgumentException.class, () -> {
319            OrderBuilder ob = account.newOrder().notBefore(someInstant);
320            ob.autoRenewal();
321        }, "accepted autoRenewal");
322
323        assertThrows(IllegalArgumentException.class, () -> {
324            OrderBuilder ob = account.newOrder().notBefore(someInstant);
325            ob.autoRenewalStart(someInstant);
326        }, "accepted autoRenewalStart");
327
328        assertThrows(IllegalArgumentException.class, () -> {
329            OrderBuilder ob = account.newOrder().notBefore(someInstant);
330            ob.autoRenewalEnd(someInstant);
331        }, "accepted autoRenewalEnd");
332
333        assertThrows(IllegalArgumentException.class, () -> {
334            OrderBuilder ob = account.newOrder().notBefore(someInstant);
335            ob.autoRenewalLifetime(Duration.ofDays(7));
336        }, "accepted autoRenewalLifetime");
337
338        provider.close();
339    }
340
341    /**
342     * Test that a new profile {@link Order} can be created.
343     */
344    @Test
345    public void testProfileOrderCertificate() throws Exception {
346        var provider = new TestableConnectionProvider() {
347            @Override
348            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
349                assertThat(url).isEqualTo(resourceUrl);
350                assertThatJson(claims.toString()).isEqualTo(getJSON("requestProfileOrderRequest").toString());
351                assertThat(login).isNotNull();
352                return HttpURLConnection.HTTP_CREATED;
353            }
354
355            @Override
356            public JSON readJsonResponse() {
357                return getJSON("requestProfileOrderResponse");
358            }
359
360            @Override
361            public URL getLocation() {
362                return locationUrl;
363            }
364        };
365
366        var login = provider.createLogin();
367
368        provider.putMetadata("profiles",JSON.parse(
369                "{\"classic\": \"The same profile you're accustomed to\"}"
370        ).toMap());
371        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
372
373        var account = new Account(login);
374        var order = account.newOrder()
375                .domain("example.org")
376                .profile("classic")
377                .create();
378
379        try (var softly = new AutoCloseableSoftAssertions()) {
380            softly.assertThat(order.getProfile()).isEqualTo("classic");
381        }
382
383        provider.close();
384    }
385
386    /**
387     * Test that a profile {@link Order} cannot be created if the profile is unsupported
388     * by the CA.
389     */
390    @Test
391    public void testUnsupportedProfileOrderCertificateFails() throws Exception {
392        var provider = new TestableConnectionProvider();
393        provider.putMetadata("profiles",JSON.parse(
394                "{\"classic\": \"The same profile you're accustomed to\"}"
395        ).toMap());
396        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
397
398        var login = provider.createLogin();
399
400        var account = new Account(login);
401        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> {
402            account.newOrder()
403                    .domain("example.org")
404                    .profile("invalid")
405                    .create();
406        }).withMessage("Server does not support profile: invalid");
407        provider.close();
408    }
409
410    /**
411     * Test that a profile {@link Order} cannot be created if the feature is unsupported
412     * by the CA.
413     */
414    @Test
415    public void testProfileOrderCertificateFails() throws IOException {
416        var provider = new TestableConnectionProvider();
417        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
418
419        var login = provider.createLogin();
420
421        var account = new Account(login);
422        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> {
423            account.newOrder()
424                    .domain("example.org")
425                    .profile("classic")
426                    .create();
427        }).withMessage("Server does not support profile");
428
429        provider.close();
430    }
431
432    /**
433     * Test that the ARI replaces field is set.
434     */
435    @Test
436    public void testARIReplaces() throws Exception {
437        var provider = new TestableConnectionProvider() {
438            @Override
439            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
440                assertThat(url).isEqualTo(resourceUrl);
441                assertThatJson(claims.toString()).isEqualTo(getJSON("requestReplacesRequest").toString());
442                assertThat(login).isNotNull();
443                return HttpURLConnection.HTTP_CREATED;
444            }
445
446            @Override
447            public JSON readJsonResponse() {
448                return getJSON("requestReplacesResponse");
449            }
450
451            @Override
452            public URL getLocation() {
453                return locationUrl;
454            }
455        };
456
457        var login = provider.createLogin();
458
459        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
460        provider.putTestResource(Resource.RENEWAL_INFO, resourceUrl);
461
462        var account = new Account(login);
463        account.newOrder()
464                .domain("example.org")
465                .replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE")
466                .create();
467
468        provider.close();
469    }
470
471    /**
472     * Test that exception is thrown if the ARI replaces field is set but ARI is not
473     * supported.
474     */
475    @Test
476    public void testARIReplaceFails() throws Exception {
477        var provider = new TestableConnectionProvider() {
478            @Override
479            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
480                fail("Request was sent");
481                return HttpURLConnection.HTTP_FORBIDDEN;
482            }
483        };
484
485        var login = provider.createLogin();
486
487        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
488
489        var account = new Account(login);
490        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> {
491                    account.newOrder()
492                            .domain("example.org")
493                            .replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE")
494                            .create();
495                })
496                .withMessage("Server does not support renewal-information");
497
498        provider.close();
499    }
500
501}