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.assertThat;
018import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
019import static org.junit.jupiter.api.Assertions.assertThrows;
020import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;
021import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
022import static org.shredzone.acme4j.toolbox.TestUtils.url;
023
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.assertThat(order.getLocation()).isEqualTo(locationUrl);
121            softly.assertThat(order.getAuthorizations()).isNotNull();
122            softly.assertThat(order.getAuthorizations()).hasSize(2);
123        }
124
125        provider.close();
126    }
127
128    /**
129     * Test that a new auto-renewal {@link Order} can be created.
130     */
131    @Test
132    public void testAutoRenewOrderCertificate() throws Exception {
133        var autoRenewStart = parseTimestamp("2018-01-01T00:00:00Z");
134        var autoRenewEnd = parseTimestamp("2019-01-01T00:00:00Z");
135        var validity = Duration.ofDays(7);
136        var predate = Duration.ofDays(6);
137
138        var provider = new TestableConnectionProvider() {
139            @Override
140            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
141                assertThat(url).isEqualTo(resourceUrl);
142                assertThatJson(claims.toString()).isEqualTo(getJSON("requestAutoRenewOrderRequest").toString());
143                assertThat(login).isNotNull();
144                return HttpURLConnection.HTTP_CREATED;
145            }
146
147            @Override
148            public JSON readJsonResponse() {
149                return getJSON("requestAutoRenewOrderResponse");
150            }
151
152            @Override
153            public URL getLocation() {
154                return locationUrl;
155            }
156        };
157
158        var login = provider.createLogin();
159
160        provider.putMetadata("auto-renewal", JSON.empty());
161        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
162
163        var account = new Account(login);
164        var order = account.newOrder()
165                        .domain("example.org")
166                        .autoRenewal()
167                        .autoRenewalStart(autoRenewStart)
168                        .autoRenewalEnd(autoRenewEnd)
169                        .autoRenewalLifetime(validity)
170                        .autoRenewalLifetimeAdjust(predate)
171                        .autoRenewalEnableGet()
172                        .replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE")
173                        .create();
174
175        try (var softly = new AutoCloseableSoftAssertions()) {
176            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(Identifier.dns("example.org"));
177            softly.assertThat(order.getNotBefore()).isEmpty();
178            softly.assertThat(order.getNotAfter()).isEmpty();
179            softly.assertThat(order.isAutoRenewing()).isTrue();
180            softly.assertThat(order.getAutoRenewalStartDate().orElseThrow()).isEqualTo(autoRenewStart);
181            softly.assertThat(order.getAutoRenewalEndDate()).isEqualTo(autoRenewEnd);
182            softly.assertThat(order.getAutoRenewalLifetime()).isEqualTo(validity);
183            softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow()).isEqualTo(predate);
184            softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue();
185            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);
186        }
187
188        provider.close();
189    }
190
191    /**
192     * Test that a new {@link Order} with ancestor domain can be created.
193     */
194    @Test
195    public void testOrderCertificateWithAncestor() throws Exception {
196        var notBefore = parseTimestamp("2016-01-01T00:00:00Z");
197        var notAfter = parseTimestamp("2016-01-08T00:00:00Z");
198
199        var provider = new TestableConnectionProvider() {
200            @Override
201            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
202                assertThat(url).isEqualTo(resourceUrl);
203                assertThatJson(claims.toString()).isEqualTo(getJSON("requestOrderRequestSub").toString());
204                assertThat(login).isNotNull();
205                return HttpURLConnection.HTTP_CREATED;
206            }
207
208            @Override
209            public JSON readJsonResponse() {
210                return getJSON("requestOrderResponseSub");
211            }
212
213            @Override
214            public URL getLocation() {
215                return locationUrl;
216            }
217        };
218
219        var login = provider.createLogin();
220
221        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
222        provider.putMetadata("subdomainAuthAllowed", true);
223
224        var account = new Account(login);
225        var order = account.newOrder()
226                .identifier(Identifier.dns("foo.bar.example.com").withAncestorDomain("example.com"))
227                .notBefore(notBefore)
228                .notAfter(notAfter)
229                .create();
230
231        try (var softly = new AutoCloseableSoftAssertions()) {
232            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(
233                    Identifier.dns("foo.bar.example.com"));
234            softly.assertThat(order.getNotBefore().orElseThrow())
235                    .isEqualTo("2016-01-01T00:10:00Z");
236            softly.assertThat(order.getNotAfter().orElseThrow())
237                    .isEqualTo("2016-01-08T00:10:00Z");
238            softly.assertThat(order.getExpires().orElseThrow())
239                    .isEqualTo("2016-01-10T00:00:00Z");
240            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);
241            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);
242            softly.assertThat(order.getAuthorizations()).isNotNull();
243            softly.assertThat(order.getAuthorizations()).hasSize(2);
244        }
245
246        provider.close();
247    }
248
249    /**
250     * Test that a new {@link Order} with ancestor domain fails if not supported.
251     */
252    @Test
253    public void testOrderCertificateWithAncestorFails() throws Exception {
254        var provider = new TestableConnectionProvider();
255
256        var login = provider.createLogin();
257
258        provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
259
260        assertThat(login.getSession().getMetadata().isSubdomainAuthAllowed()).isFalse();
261
262        var account = new Account(login);
263        assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() ->
264                account.newOrder()
265                        .identifier(Identifier.dns("foo.bar.example.com").withAncestorDomain("example.com"))
266                        .create()
267        );
268
269        provider.close();
270    }
271
272    /**
273     * Test that an auto-renewal {@link Order} cannot be created if unsupported by the CA.
274     */
275    @Test
276    public void testAutoRenewOrderCertificateFails() {
277        assertThrows(AcmeNotSupportedException.class, () -> {
278            var provider = new TestableConnectionProvider();
279            provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
280
281            var login = provider.createLogin();
282
283            var account = new Account(login);
284            account.newOrder()
285                            .domain("example.org")
286                            .autoRenewal()
287                            .create();
288
289            provider.close();
290        });
291    }
292
293    /**
294     * Test that auto-renew and notBefore/notAfter cannot be mixed.
295     */
296    @Test
297    public void testAutoRenewNotMixed() throws Exception {
298        var someInstant = parseTimestamp("2018-01-01T00:00:00Z");
299
300        var provider = new TestableConnectionProvider();
301        var login = provider.createLogin();
302
303        var account = new Account(login);
304
305        assertThrows(IllegalArgumentException.class, () -> {
306            OrderBuilder ob = account.newOrder().autoRenewal();
307            ob.notBefore(someInstant);
308        }, "accepted notBefore");
309
310        assertThrows(IllegalArgumentException.class, () -> {
311            OrderBuilder ob = account.newOrder().autoRenewal();
312            ob.notAfter(someInstant);
313        }, "accepted notAfter");
314
315        assertThrows(IllegalArgumentException.class, () -> {
316            OrderBuilder ob = account.newOrder().notBefore(someInstant);
317            ob.autoRenewal();
318        }, "accepted autoRenewal");
319
320        assertThrows(IllegalArgumentException.class, () -> {
321            OrderBuilder ob = account.newOrder().notBefore(someInstant);
322            ob.autoRenewalStart(someInstant);
323        }, "accepted autoRenewalStart");
324
325        assertThrows(IllegalArgumentException.class, () -> {
326            OrderBuilder ob = account.newOrder().notBefore(someInstant);
327            ob.autoRenewalEnd(someInstant);
328        }, "accepted autoRenewalEnd");
329
330        assertThrows(IllegalArgumentException.class, () -> {
331            OrderBuilder ob = account.newOrder().notBefore(someInstant);
332            ob.autoRenewalLifetime(Duration.ofDays(7));
333        }, "accepted autoRenewalLifetime");
334
335        provider.close();
336    }
337
338}