001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2023 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 org.assertj.core.api.Assertions.assertThat;
017import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
018import static org.mockito.Mockito.mock;
019import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
020import static org.shredzone.acme4j.toolbox.TestUtils.url;
021
022import java.net.HttpURLConnection;
023import java.net.URL;
024import java.time.Duration;
025import java.time.Instant;
026import java.time.ZonedDateTime;
027import java.time.temporal.ChronoUnit;
028import java.util.Optional;
029
030import org.assertj.core.api.AutoCloseableSoftAssertions;
031import org.junit.jupiter.api.Test;
032import org.shredzone.acme4j.exception.AcmeProtocolException;
033import org.shredzone.acme4j.provider.TestableConnectionProvider;
034import org.shredzone.acme4j.toolbox.JSON;
035
036/**
037 * Unit test for {@link RenewalInfo}.
038 */
039public class RenewalInfoTest {
040
041    private final URL locationUrl = url("http://example.com/acme/renewalInfo/1234");
042    private final Instant retryAfterInstant = Instant.now().plus(10L, ChronoUnit.DAYS);
043    private final Instant startWindow = Instant.parse("2021-01-03T00:00:00Z");
044    private final Instant endWindow = Instant.parse("2021-01-07T00:00:00Z");
045
046    @Test
047    public void testGetters() throws Exception {
048        var provider = new TestableConnectionProvider() {
049            @Override
050            public int sendRequest(URL url, Session session, ZonedDateTime ifModifiedSince) {
051                assertThat(url).isEqualTo(locationUrl);
052                assertThat(session).isNotNull();
053                assertThat(ifModifiedSince).isNull();
054                return HttpURLConnection.HTTP_OK;
055            }
056
057            @Override
058            public JSON readJsonResponse() {
059                return getJSON("renewalInfo");
060            }
061
062            @Override
063            public Optional<Instant> getRetryAfter() {
064                return Optional.of(retryAfterInstant);
065            }
066        };
067
068        var login = provider.createLogin();
069
070        var renewalInfo = new RenewalInfo(login, locationUrl);
071        var recheckAfter = renewalInfo.fetch();
072        assertThat(recheckAfter).hasValue(retryAfterInstant);
073
074        // Check getters
075        try (var softly = new AutoCloseableSoftAssertions()) {
076            softly.assertThat(renewalInfo.getLocation()).isEqualTo(locationUrl);
077            softly.assertThat(renewalInfo.getSuggestedWindowStart())
078                    .isEqualTo(startWindow);
079            softly.assertThat(renewalInfo.getSuggestedWindowEnd())
080                    .isEqualTo(endWindow);
081            softly.assertThat(renewalInfo.getExplanation())
082                    .isNotEmpty()
083                    .contains(url("https://example.com/docs/example-mass-reissuance-event"));
084        }
085
086        // Check renewalIsNotRequired
087        try (var softly = new AutoCloseableSoftAssertions()) {
088            softly.assertThat(renewalInfo.renewalIsNotRequired(startWindow.minusSeconds(1L)))
089                    .isTrue();
090            softly.assertThat(renewalInfo.renewalIsNotRequired(startWindow))
091                    .isFalse();
092            softly.assertThat(renewalInfo.renewalIsNotRequired(endWindow.minusSeconds(1L)))
093                    .isFalse();
094            softly.assertThat(renewalInfo.renewalIsNotRequired(endWindow))
095                    .isFalse();
096        }
097
098        // Check renewalIsRecommended
099        try (var softly = new AutoCloseableSoftAssertions()) {
100            softly.assertThat(renewalInfo.renewalIsRecommended(startWindow.minusSeconds(1L)))
101                    .isFalse();
102            softly.assertThat(renewalInfo.renewalIsRecommended(startWindow))
103                    .isTrue();
104            softly.assertThat(renewalInfo.renewalIsRecommended(endWindow.minusSeconds(1L)))
105                    .isTrue();
106            softly.assertThat(renewalInfo.renewalIsRecommended(endWindow))
107                    .isFalse();
108        }
109
110        // Check renewalIsOverdue
111        try (var softly = new AutoCloseableSoftAssertions()) {
112            softly.assertThat(renewalInfo.renewalIsOverdue(startWindow.minusSeconds(1L)))
113                    .isFalse();
114            softly.assertThat(renewalInfo.renewalIsOverdue(startWindow))
115                    .isFalse();
116            softly.assertThat(renewalInfo.renewalIsOverdue(endWindow.minusSeconds(1L)))
117                    .isFalse();
118            softly.assertThat(renewalInfo.renewalIsOverdue(endWindow))
119                    .isTrue();
120        }
121
122        // Check getRandomProposal, is empty because end window is in the past
123        var proposal = renewalInfo.getRandomProposal(null);
124        assertThat(proposal).isEmpty();
125
126        provider.close();
127    }
128
129    @Test
130    public void testRandomProposal() {
131        var login = mock(Login.class);
132        var start = Instant.now();
133        var end = start.plus(1L, ChronoUnit.DAYS);
134
135        var renewalInfo = new RenewalInfo(login, locationUrl) {
136            @Override
137            public Instant getSuggestedWindowStart() {
138                return start;
139            }
140
141            @Override
142            public Instant getSuggestedWindowEnd() {
143                return end;
144            }
145        };
146
147        var noFreq = renewalInfo.getRandomProposal(null);
148        assertThat(noFreq).isNotEmpty();
149        assertThat(noFreq.get()).isBetween(start, end);
150
151        var oneHour = renewalInfo.getRandomProposal(Duration.ofHours(1L));
152        assertThat(oneHour).isNotEmpty();
153        assertThat(oneHour.get()).isBetween(start, end.minus(1L, ChronoUnit.HOURS));
154
155        var twoDays = renewalInfo.getRandomProposal(Duration.ofDays(2L));
156        assertThat(twoDays).isEmpty();
157    }
158
159    @Test
160    public void testDateAssertion() {
161        var login = mock(Login.class);
162        var start = Instant.now();
163        var end = start.minusSeconds(1L);  // end before start
164
165        var renewalInfo = new RenewalInfo(login, locationUrl) {
166            @Override
167            public Instant getSuggestedWindowStart() {
168                return start;
169            }
170
171            @Override
172            public Instant getSuggestedWindowEnd() {
173                return end;
174            }
175        };
176
177        assertThatExceptionOfType(AcmeProtocolException.class)
178                .isThrownBy(() -> renewalInfo.renewalIsRecommended(start));
179    }
180
181}