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.shredzone.acme4j.toolbox.TestUtils.getJSON;
019import static org.shredzone.acme4j.toolbox.TestUtils.url;
020
021import java.net.HttpURLConnection;
022import java.net.URI;
023import java.net.URL;
024import java.time.Duration;
025import java.util.concurrent.atomic.AtomicBoolean;
026
027import org.assertj.core.api.AutoCloseableSoftAssertions;
028import org.junit.jupiter.api.Test;
029import org.shredzone.acme4j.exception.AcmeNotSupportedException;
030import org.shredzone.acme4j.provider.TestableConnectionProvider;
031import org.shredzone.acme4j.toolbox.JSON;
032import org.shredzone.acme4j.toolbox.JSONBuilder;
033import org.shredzone.acme4j.toolbox.TestUtils;
034
035/**
036 * Unit tests for {@link Order}.
037 */
038public class OrderTest {
039
040    private final URL locationUrl = url("http://example.com/acme/order/1234");
041    private final URL finalizeUrl = url("https://example.com/acme/acct/1/order/1/finalize");
042
043    /**
044     * Test that order is properly updated.
045     */
046    @Test
047    public void testUpdate() throws Exception {
048        var provider = new TestableConnectionProvider() {
049            @Override
050            public int sendSignedPostAsGetRequest(URL url, Login login) {
051                assertThat(url).isEqualTo(locationUrl);
052                return HttpURLConnection.HTTP_OK;
053            }
054
055            @Override
056            public JSON readJsonResponse() {
057                return getJSON("updateOrderResponse");
058            }
059        };
060
061        var login = provider.createLogin();
062
063        var order = new Order(login, locationUrl);
064        order.update();
065
066        try (var softly = new AutoCloseableSoftAssertions()) {
067            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);
068            softly.assertThat(order.getExpires().orElseThrow()).isEqualTo("2015-03-01T14:09:00Z");
069            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);
070
071            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(
072                    Identifier.dns("example.com"),
073                    Identifier.dns("www.example.com"));
074            softly.assertThat(order.getNotBefore().orElseThrow())
075                    .isEqualTo("2016-01-01T00:00:00Z");
076            softly.assertThat(order.getNotAfter().orElseThrow())
077                    .isEqualTo("2016-01-08T00:00:00Z");
078            softly.assertThat(order.getCertificate().getLocation())
079                    .isEqualTo(url("https://example.com/acme/cert/1234"));
080            softly.assertThat(order.getFinalizeLocation()).isEqualTo(finalizeUrl);
081
082            softly.assertThat(order.isAutoRenewing()).isFalse();
083            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
084                    .isThrownBy(order::getAutoRenewalStartDate);
085            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
086                    .isThrownBy(order::getAutoRenewalEndDate);
087            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
088                    .isThrownBy(order::getAutoRenewalLifetime);
089            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
090                    .isThrownBy(order::getAutoRenewalLifetimeAdjust);
091            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
092                    .isThrownBy(order::isAutoRenewalGetEnabled);
093            softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
094                    .isThrownBy(order::getProfile);
095
096            softly.assertThat(order.getError()).isNotEmpty();
097            softly.assertThat(order.getError().orElseThrow().getType())
098                    .isEqualTo(URI.create("urn:ietf:params:acme:error:connection"));
099            softly.assertThat(order.getError().flatMap(Problem::getDetail).orElseThrow())
100                    .isEqualTo("connection refused");
101
102            var auths = order.getAuthorizations();
103            softly.assertThat(auths).hasSize(2);
104            softly.assertThat(auths.stream())
105                    .map(Authorization::getLocation)
106                    .containsExactlyInAnyOrder(
107                            url("https://example.com/acme/authz/1234"),
108                            url("https://example.com/acme/authz/2345"));
109        }
110
111        provider.close();
112    }
113
114    /**
115     * Test lazy loading.
116     */
117    @Test
118    public void testLazyLoading() throws Exception {
119        var requestWasSent = new AtomicBoolean(false);
120
121        var provider = new TestableConnectionProvider() {
122            @Override
123            public int sendSignedPostAsGetRequest(URL url, Login login) {
124                requestWasSent.set(true);
125                assertThat(url).isEqualTo(locationUrl);
126                return HttpURLConnection.HTTP_OK;
127            }
128
129            @Override
130            public JSON readJsonResponse() {
131                return getJSON("updateOrderResponse");
132            }
133        };
134
135        var login = provider.createLogin();
136
137        var order = new Order(login, locationUrl);
138
139        try (var softly = new AutoCloseableSoftAssertions()) {
140            // Lazy loading
141            softly.assertThat(requestWasSent).isFalse();
142            softly.assertThat(order.getCertificate().getLocation())
143                    .isEqualTo(url("https://example.com/acme/cert/1234"));
144            softly.assertThat(requestWasSent).isTrue();
145
146            // Subsequent queries do not trigger another load
147            requestWasSent.set(false);
148            softly.assertThat(order.getCertificate().getLocation())
149                    .isEqualTo(url("https://example.com/acme/cert/1234"));
150            softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING);
151            softly.assertThat(order.getExpires().orElseThrow()).isEqualTo("2015-03-01T14:09:00Z");
152            softly.assertThat(requestWasSent).isFalse();
153        }
154
155        provider.close();
156    }
157
158    /**
159     * Test that order is properly finalized.
160     */
161    @Test
162    public void testFinalize() throws Exception {
163        var csr = TestUtils.getResourceAsByteArray("/csr.der");
164
165        var provider = new TestableConnectionProvider() {
166            private boolean isFinalized = false;
167
168            @Override
169            public int sendSignedPostAsGetRequest(URL url, Login login) {
170                assertThat(url).isEqualTo(locationUrl);
171                return HttpURLConnection.HTTP_OK;
172            }
173
174            @Override
175            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
176                assertThat(url).isEqualTo(finalizeUrl);
177                assertThatJson(claims.toString()).isEqualTo(getJSON("finalizeRequest").toString());
178                assertThat(login).isNotNull();
179                isFinalized = true;
180                return HttpURLConnection.HTTP_OK;
181            }
182
183            @Override
184            public JSON readJsonResponse() {
185                return getJSON(isFinalized ? "finalizeResponse" : "updateOrderResponse");
186            }
187        };
188
189        var login = provider.createLogin();
190
191        var order = new Order(login, locationUrl);
192        order.execute(csr);
193
194        try (var softly = new AutoCloseableSoftAssertions()) {
195            softly.assertThat(order.getStatus()).isEqualTo(Status.VALID);
196            softly.assertThat(order.getExpires().orElseThrow()).isEqualTo("2015-03-01T14:09:00Z");
197            softly.assertThat(order.getLocation()).isEqualTo(locationUrl);
198
199            softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(
200                    Identifier.dns("example.com"),
201                    Identifier.dns("www.example.com"));
202            softly.assertThat(order.getNotBefore().orElseThrow())
203                    .isEqualTo("2016-01-01T00:00:00Z");
204            softly.assertThat(order.getNotAfter().orElseThrow())
205                    .isEqualTo("2016-01-08T00:00:00Z");
206            softly.assertThat(order.isAutoRenewalCertificate()).isFalse();
207            softly.assertThat(order.getCertificate().getLocation())
208                    .isEqualTo(url("https://example.com/acme/cert/1234"));
209            softly.assertThatIllegalStateException()
210                    .isThrownBy(order::getAutoRenewalCertificate);
211            softly.assertThat(order.getFinalizeLocation()).isEqualTo(finalizeUrl);
212
213            var auths = order.getAuthorizations();
214            softly.assertThat(auths).hasSize(2);
215            softly.assertThat(auths.stream())
216                    .map(Authorization::getLocation)
217                    .containsExactlyInAnyOrder(
218                            url("https://example.com/acme/authz/1234"),
219                            url("https://example.com/acme/authz/2345"));
220        }
221
222        provider.close();
223    }
224
225    /**
226     * Test that order is properly updated.
227     */
228    @Test
229    public void testAutoRenewUpdate() throws Exception {
230        var provider = new TestableConnectionProvider() {
231            @Override
232            public int sendSignedPostAsGetRequest(URL url, Login login) {
233                assertThat(url).isEqualTo(locationUrl);
234                return HttpURLConnection.HTTP_OK;
235            }
236
237            @Override
238            public JSON readJsonResponse() {
239                return getJSON("updateAutoRenewOrderResponse");
240            }
241        };
242
243        provider.putMetadata("auto-renewal", JSON.empty());
244
245        var login = provider.createLogin();
246
247        var order = new Order(login, locationUrl);
248        order.update();
249
250        try (var softly = new AutoCloseableSoftAssertions()) {
251            softly.assertThat(order.isAutoRenewing()).isTrue();
252            softly.assertThat(order.getAutoRenewalStartDate().orElseThrow())
253                    .isEqualTo("2016-01-01T00:00:00Z");
254            softly.assertThat(order.getAutoRenewalEndDate())
255                    .isEqualTo("2017-01-01T00:00:00Z");
256            softly.assertThat(order.getAutoRenewalLifetime())
257                    .isEqualTo(Duration.ofHours(168));
258            softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow())
259                    .isEqualTo(Duration.ofDays(6));
260            softly.assertThat(order.getNotBefore()).isEmpty();
261            softly.assertThat(order.getNotAfter()).isEmpty();
262            softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue();
263        }
264
265        provider.close();
266    }
267
268    /**
269     * Test that auto-renew order is properly finalized.
270     */
271    @Test
272    public void testAutoRenewFinalize() throws Exception {
273        var provider = new TestableConnectionProvider() {
274            @Override
275            public int sendSignedPostAsGetRequest(URL url, Login login) {
276                assertThat(url).isEqualTo(locationUrl);
277                return HttpURLConnection.HTTP_OK;
278            }
279
280            @Override
281            public JSON readJsonResponse() {
282                return getJSON("finalizeAutoRenewResponse");
283            }
284        };
285
286        var login = provider.createLogin();
287        var order = login.bindOrder(locationUrl);
288
289        try (var softly = new AutoCloseableSoftAssertions()) {
290            softly.assertThat(order.isAutoRenewalCertificate()).isTrue();
291            softly.assertThat(order.getCertificate().getLocation())
292                    .isEqualTo(url("https://example.com/acme/cert/1234"));
293            softly.assertThat(order.getAutoRenewalCertificate().getLocation())
294                    .isEqualTo(url("https://example.com/acme/cert/1234"));
295            softly.assertThat(order.isAutoRenewing()).isTrue();
296            softly.assertThat(order.getAutoRenewalStartDate().orElseThrow())
297                    .isEqualTo("2018-01-01T00:00:00Z");
298            softly.assertThat(order.getAutoRenewalEndDate())
299                    .isEqualTo("2019-01-01T00:00:00Z");
300            softly.assertThat(order.getAutoRenewalLifetime())
301                    .isEqualTo(Duration.ofHours(168));
302            softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow())
303                    .isEqualTo(Duration.ofDays(6));
304            softly.assertThat(order.getNotBefore()).isEmpty();
305            softly.assertThat(order.getNotAfter()).isEmpty();
306            softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue();
307        }
308
309        provider.close();
310    }
311
312    /**
313     * Test that auto-renew order is properly canceled.
314     */
315    @Test
316    public void testCancel() throws Exception {
317        var provider = new TestableConnectionProvider() {
318            @Override
319            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
320                var json = claims.toJSON();
321                assertThat(json.get("status").asString()).isEqualTo("canceled");
322                assertThat(url).isEqualTo(locationUrl);
323                assertThat(login).isNotNull();
324                return HttpURLConnection.HTTP_OK;
325            }
326
327            @Override
328            public JSON readJsonResponse() {
329                return getJSON("canceledOrderResponse");
330            }
331        };
332
333        provider.putMetadata("auto-renewal", JSON.empty());
334
335        var login = provider.createLogin();
336
337        var order = new Order(login, locationUrl);
338        order.cancelAutoRenewal();
339
340        assertThat(order.getStatus()).isEqualTo(Status.CANCELED);
341
342        provider.close();
343    }
344
345}