001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2015 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.challenge;
015
016import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
017import static org.assertj.core.api.Assertions.assertThat;
018import static org.assertj.core.api.Assertions.within;
019import static org.junit.jupiter.api.Assertions.assertThrows;
020import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
021import static org.shredzone.acme4j.toolbox.TestUtils.url;
022
023import java.net.HttpURLConnection;
024import java.net.URI;
025import java.net.URL;
026import java.time.Duration;
027import java.time.Instant;
028import java.time.temporal.ChronoUnit;
029import java.util.Optional;
030
031import org.assertj.core.api.AutoCloseableSoftAssertions;
032import org.junit.jupiter.api.Test;
033import org.shredzone.acme4j.Login;
034import org.shredzone.acme4j.Status;
035import org.shredzone.acme4j.exception.AcmeProtocolException;
036import org.shredzone.acme4j.provider.TestableConnectionProvider;
037import org.shredzone.acme4j.toolbox.JSON;
038import org.shredzone.acme4j.toolbox.JSONBuilder;
039import org.shredzone.acme4j.toolbox.TestUtils;
040
041/**
042 * Unit tests for {@link Challenge}.
043 */
044public class ChallengeTest {
045    private final URL locationUrl = url("https://example.com/acme/some-location");
046
047    /**
048     * Test that after unmarshaling, the challenge properties are set correctly.
049     */
050    @Test
051    public void testUnmarshal() {
052        var challenge = new Challenge(TestUtils.login(), getJSON("genericChallenge"));
053
054        // Test unmarshalled values
055        try (var softly = new AutoCloseableSoftAssertions()) {
056            softly.assertThat(challenge.getType()).isEqualTo("generic-01");
057            softly.assertThat(challenge.getStatus()).isEqualTo(Status.INVALID);
058            softly.assertThat(challenge.getLocation()).isEqualTo(url("http://example.com/challenge/123"));
059            softly.assertThat(challenge.getValidated().orElseThrow())
060                    .isCloseTo("2015-12-12T17:19:36.336Z", within(1, ChronoUnit.MILLIS));
061            softly.assertThat(challenge.getJSON().get("type").asString()).isEqualTo("generic-01");
062            softly.assertThat(challenge.getJSON().get("url").asURL()).isEqualTo(url("http://example.com/challenge/123"));
063
064            var error = challenge.getError().orElseThrow();
065            softly.assertThat(error.getType()).isEqualTo(URI.create("urn:ietf:params:acme:error:incorrectResponse"));
066            softly.assertThat(error.getDetail().orElseThrow()).isEqualTo("bad token");
067            softly.assertThat(error.getInstance().orElseThrow())
068                    .isEqualTo(URI.create("http://example.com/documents/faq.html"));
069        }
070    }
071
072    /**
073     * Test that {@link Challenge#prepareResponse(JSONBuilder)} contains the type.
074     */
075    @Test
076    public void testRespond() {
077        var challenge = new Challenge(TestUtils.login(), getJSON("genericChallenge"));
078
079        var response = new JSONBuilder();
080        challenge.prepareResponse(response);
081
082        assertThatJson(response.toString()).isEqualTo("{}");
083    }
084
085    /**
086     * Test that an exception is thrown on challenge type mismatch.
087     */
088    @Test
089    public void testNotAcceptable() {
090        assertThrows(AcmeProtocolException.class, () ->
091            new Http01Challenge(TestUtils.login(), getJSON("dnsChallenge"))
092        );
093    }
094
095    /**
096     * Test that a challenge can be triggered.
097     */
098    @Test
099    public void testTrigger() throws Exception {
100        var provider = new TestableConnectionProvider() {
101            @Override
102            public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
103                assertThat(url).isEqualTo(locationUrl);
104                assertThatJson(claims.toString()).isEqualTo(getJSON("triggerHttpChallengeRequest").toString());
105                assertThat(login).isNotNull();
106                return HttpURLConnection.HTTP_OK;
107            }
108
109            @Override
110            public JSON readJsonResponse() {
111                return getJSON("triggerHttpChallengeResponse");
112            }
113        };
114
115        var login = provider.createLogin();
116
117        var challenge = new Http01Challenge(login, getJSON("triggerHttpChallenge"));
118
119        challenge.trigger();
120
121        assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);
122        assertThat(challenge.getLocation()).isEqualTo(locationUrl);
123
124        provider.close();
125    }
126
127    /**
128     * Test that a challenge is properly updated.
129     */
130    @Test
131    public void testUpdate() throws Exception {
132        var provider = new TestableConnectionProvider() {
133            @Override
134            public int sendSignedPostAsGetRequest(URL url, Login login) {
135                assertThat(url).isEqualTo(locationUrl);
136                return HttpURLConnection.HTTP_OK;
137            }
138
139            @Override
140            public JSON readJsonResponse() {
141                return getJSON("updateHttpChallengeResponse");
142            }
143        };
144
145        var login = provider.createLogin();
146
147        var challenge = new Http01Challenge(login, getJSON("triggerHttpChallengeResponse"));
148
149        challenge.update();
150
151        assertThat(challenge.getStatus()).isEqualTo(Status.VALID);
152        assertThat(challenge.getLocation()).isEqualTo(locationUrl);
153
154        provider.close();
155    }
156
157    /**
158     * Test that a challenge is properly updated, with Retry-After header.
159     */
160    @Test
161    public void testUpdateRetryAfter() throws Exception {
162        var retryAfter = Instant.now().plus(Duration.ofSeconds(30));
163
164        var provider = new TestableConnectionProvider() {
165            @Override
166            public int sendSignedPostAsGetRequest(URL url, Login login) {
167                assertThat(url).isEqualTo(locationUrl);
168                return HttpURLConnection.HTTP_OK;
169            }
170
171            @Override
172            public JSON readJsonResponse() {
173                return getJSON("updateHttpChallengeResponse");
174            }
175
176            @Override
177            public Optional<Instant> getRetryAfter() {
178                return Optional.of(retryAfter);
179            }
180        };
181
182        var login = provider.createLogin();
183
184        var challenge = new Http01Challenge(login, getJSON("triggerHttpChallengeResponse"));
185        var returnedRetryAfter = challenge.fetch();
186        assertThat(returnedRetryAfter).hasValue(retryAfter);
187
188        assertThat(challenge.getStatus()).isEqualTo(Status.VALID);
189        assertThat(challenge.getLocation()).isEqualTo(locationUrl);
190
191        provider.close();
192    }
193
194    /**
195     * Test that unmarshalling something different like a challenge fails.
196     */
197    @Test
198    public void testBadUnmarshall() {
199        assertThrows(AcmeProtocolException.class, () ->
200            new Challenge(TestUtils.login(), getJSON("updateAccountResponse"))
201        );
202    }
203
204}