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.provider; 015 016import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; 017import static org.assertj.core.api.Assertions.assertThat; 018import static org.junit.jupiter.api.Assertions.assertThrows; 019import static org.mockito.Mockito.*; 020import static org.shredzone.acme4j.toolbox.TestUtils.getJSON; 021 022import java.net.HttpURLConnection; 023import java.net.URI; 024import java.net.URL; 025import java.time.ZonedDateTime; 026import java.time.temporal.ChronoUnit; 027import java.util.Optional; 028import java.util.concurrent.atomic.AtomicBoolean; 029 030import org.junit.jupiter.api.Test; 031import org.shredzone.acme4j.Login; 032import org.shredzone.acme4j.Session; 033import org.shredzone.acme4j.challenge.Challenge; 034import org.shredzone.acme4j.challenge.Dns01Challenge; 035import org.shredzone.acme4j.challenge.Http01Challenge; 036import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; 037import org.shredzone.acme4j.challenge.TokenChallenge; 038import org.shredzone.acme4j.connector.Connection; 039import org.shredzone.acme4j.connector.DefaultConnection; 040import org.shredzone.acme4j.connector.HttpConnector; 041import org.shredzone.acme4j.connector.NetworkSettings; 042import org.shredzone.acme4j.exception.AcmeException; 043import org.shredzone.acme4j.exception.AcmeProtocolException; 044import org.shredzone.acme4j.toolbox.JSONBuilder; 045import org.shredzone.acme4j.toolbox.TestUtils; 046 047/** 048 * Unit tests for {@link AbstractAcmeProvider}. 049 */ 050public class AbstractAcmeProviderTest { 051 052 private static final URI SERVER_URI = URI.create("http://example.com/acme"); 053 private static final URL RESOLVED_URL = TestUtils.url("http://example.com/acme/directory"); 054 private static final NetworkSettings NETWORK_SETTINGS = new NetworkSettings(); 055 056 /** 057 * Test that connect returns a connection. 058 */ 059 @Test 060 public void testConnect() { 061 var invoked = new AtomicBoolean(); 062 063 var provider = new TestAbstractAcmeProvider() { 064 @Override 065 protected HttpConnector createHttpConnector(NetworkSettings settings) { 066 assertThat(settings).isSameAs(NETWORK_SETTINGS); 067 invoked.set(true); 068 return super.createHttpConnector(settings); 069 } 070 }; 071 072 var connection = provider.connect(SERVER_URI, NETWORK_SETTINGS); 073 assertThat(connection).isNotNull(); 074 assertThat(connection).isInstanceOf(DefaultConnection.class); 075 assertThat(invoked).isTrue(); 076 } 077 078 /** 079 * Verify that the resources directory is read. 080 */ 081 @Test 082 public void testResources() throws AcmeException { 083 var connection = mock(Connection.class); 084 var session = mock(Session.class); 085 086 when(connection.readJsonResponse()).thenReturn(getJSON("directory")); 087 088 var provider = new TestAbstractAcmeProvider(connection); 089 var map = provider.directory(session, SERVER_URI); 090 091 assertThatJson(map.toString()).isEqualTo(TestUtils.getJSON("directory").toString()); 092 093 verify(connection).sendRequest(RESOLVED_URL, session, null); 094 verify(connection).getNonce(); 095 verify(connection).getLastModified(); 096 verify(connection).getExpiration(); 097 verify(connection).readJsonResponse(); 098 verify(connection).close(); 099 verifyNoMoreInteractions(connection); 100 } 101 102 /** 103 * Verify that the cache control headers are evaluated. 104 */ 105 @Test 106 public void testResourcesCacheControl() throws AcmeException { 107 var lastModified = ZonedDateTime.now().minus(13, ChronoUnit.DAYS); 108 var expiryDate = ZonedDateTime.now().plus(60, ChronoUnit.DAYS); 109 110 var connection = mock(Connection.class); 111 var session = mock(Session.class); 112 113 when(connection.readJsonResponse()).thenReturn(getJSON("directory")); 114 when(connection.getLastModified()).thenReturn(Optional.of(lastModified)); 115 when(connection.getExpiration()).thenReturn(Optional.of(expiryDate)); 116 when(session.getDirectoryExpires()).thenReturn(null); 117 when(session.getDirectoryLastModified()).thenReturn(null); 118 119 var provider = new TestAbstractAcmeProvider(connection); 120 var map = provider.directory(session, SERVER_URI); 121 122 assertThatJson(map.toString()).isEqualTo(TestUtils.getJSON("directory").toString()); 123 124 verify(session).setDirectoryLastModified(eq(lastModified)); 125 verify(session).setDirectoryExpires(eq(expiryDate)); 126 verify(session).getDirectoryExpires(); 127 verify(session).getDirectoryLastModified(); 128 verify(session).networkSettings(); 129 verifyNoMoreInteractions(session); 130 131 verify(connection).sendRequest(RESOLVED_URL, session, null); 132 verify(connection).getNonce(); 133 verify(connection).getLastModified(); 134 verify(connection).getExpiration(); 135 verify(connection).readJsonResponse(); 136 verify(connection).close(); 137 verifyNoMoreInteractions(connection); 138 } 139 140 /** 141 * Verify that resorces are not fetched if not yet expired. 142 */ 143 @Test 144 public void testResourcesNotExprired() throws AcmeException { 145 var expiryDate = ZonedDateTime.now().plus(60, ChronoUnit.DAYS); 146 147 var connection = mock(Connection.class); 148 var session = mock(Session.class); 149 150 when(session.getDirectoryExpires()).thenReturn(expiryDate); 151 152 var provider = new TestAbstractAcmeProvider(); 153 var map = provider.directory(session, SERVER_URI); 154 155 assertThat(map).isNull(); 156 157 verify(session).getDirectoryExpires(); 158 verifyNoMoreInteractions(session); 159 160 verifyNoMoreInteractions(connection); 161 } 162 163 /** 164 * Verify that resorces are fetched if expired. 165 */ 166 @Test 167 public void testResourcesExprired() throws AcmeException { 168 var expiryDate = ZonedDateTime.now().plus(60, ChronoUnit.DAYS); 169 var pastExpiryDate = ZonedDateTime.now().minus(10, ChronoUnit.MINUTES); 170 171 var connection = mock(Connection.class); 172 var session = mock(Session.class); 173 174 when(connection.readJsonResponse()).thenReturn(getJSON("directory")); 175 when(connection.getExpiration()).thenReturn(Optional.of(expiryDate)); 176 when(connection.getLastModified()).thenReturn(Optional.empty()); 177 when(session.getDirectoryExpires()).thenReturn(pastExpiryDate); 178 179 var provider = new TestAbstractAcmeProvider(connection); 180 var map = provider.directory(session, SERVER_URI); 181 182 assertThatJson(map.toString()).isEqualTo(TestUtils.getJSON("directory").toString()); 183 184 verify(session).setDirectoryExpires(eq(expiryDate)); 185 verify(session).setDirectoryLastModified(eq(null)); 186 verify(session).getDirectoryExpires(); 187 verify(session).getDirectoryLastModified(); 188 verify(session).networkSettings(); 189 verifyNoMoreInteractions(session); 190 191 verify(connection).sendRequest(RESOLVED_URL, session, null); 192 verify(connection).getNonce(); 193 verify(connection).getLastModified(); 194 verify(connection).getExpiration(); 195 verify(connection).readJsonResponse(); 196 verify(connection).close(); 197 verifyNoMoreInteractions(connection); 198 } 199 200 /** 201 * Verify that if-modified-since is used. 202 */ 203 @Test 204 public void testResourcesIfModifiedSince() throws AcmeException { 205 var modifiedSinceDate = ZonedDateTime.now().minus(60, ChronoUnit.DAYS); 206 207 var connection = mock(Connection.class); 208 var session = mock(Session.class); 209 210 when(connection.sendRequest(eq(RESOLVED_URL), eq(session), eq(modifiedSinceDate))) 211 .thenReturn(HttpURLConnection.HTTP_NOT_MODIFIED); 212 when(connection.getLastModified()).thenReturn(Optional.of(modifiedSinceDate)); 213 when(session.getDirectoryLastModified()).thenReturn(modifiedSinceDate); 214 215 var provider = new TestAbstractAcmeProvider(connection); 216 var map = provider.directory(session, SERVER_URI); 217 218 assertThat(map).isNull(); 219 220 verify(session).getDirectoryExpires(); 221 verify(session).getDirectoryLastModified(); 222 verify(session).networkSettings(); 223 verifyNoMoreInteractions(session); 224 225 verify(connection).sendRequest(RESOLVED_URL, session, modifiedSinceDate); 226 verify(connection).close(); 227 verifyNoMoreInteractions(connection); 228 } 229 230 /** 231 * Test that challenges are generated properly. 232 */ 233 @Test 234 public void testCreateChallenge() { 235 var login = mock(Login.class); 236 237 var provider = new TestAbstractAcmeProvider(); 238 239 var c1 = provider.createChallenge(login, getJSON("httpChallenge")); 240 assertThat(c1).isNotNull(); 241 assertThat(c1).isInstanceOf(Http01Challenge.class); 242 243 var c2 = provider.createChallenge(login, getJSON("httpChallenge")); 244 assertThat(c2).isNotSameAs(c1); 245 246 var c3 = provider.createChallenge(login, getJSON("dnsChallenge")); 247 assertThat(c3).isNotNull(); 248 assertThat(c3).isInstanceOf(Dns01Challenge.class); 249 250 var c4 = provider.createChallenge(login, getJSON("tlsAlpnChallenge")); 251 assertThat(c4).isNotNull(); 252 assertThat(c4).isInstanceOf(TlsAlpn01Challenge.class); 253 254 var json6 = new JSONBuilder() 255 .put("type", "foobar-01") 256 .put("url", "https://example.com/some/challenge") 257 .toJSON(); 258 var c6 = provider.createChallenge(login, json6); 259 assertThat(c6).isNotNull(); 260 assertThat(c6).isInstanceOf(Challenge.class); 261 262 var json7 = new JSONBuilder() 263 .put("type", "foobar-01") 264 .put("token", "abc123") 265 .put("url", "https://example.com/some/challenge") 266 .toJSON(); 267 var c7 = provider.createChallenge(login, json7); 268 assertThat(c7).isNotNull(); 269 assertThat(c7).isInstanceOf(TokenChallenge.class); 270 271 assertThrows(AcmeProtocolException.class, () -> { 272 var json8 = new JSONBuilder() 273 .put("url", "https://example.com/some/challenge") 274 .toJSON(); 275 provider.createChallenge(login, json8); 276 }); 277 278 assertThrows(NullPointerException.class, () -> provider.createChallenge(login, null)); 279 } 280 281 private static class TestAbstractAcmeProvider extends AbstractAcmeProvider { 282 private final Connection connection; 283 284 public TestAbstractAcmeProvider() { 285 this.connection = null; 286 } 287 288 public TestAbstractAcmeProvider(Connection connection) { 289 this.connection = connection; 290 } 291 292 @Override 293 public boolean accepts(URI serverUri) { 294 assertThat(serverUri).isEqualTo(SERVER_URI); 295 return true; 296 } 297 298 @Override 299 public URL resolve(URI serverUri) { 300 assertThat(serverUri).isEqualTo(SERVER_URI); 301 return RESOLVED_URL; 302 } 303 304 @Override 305 public Connection connect(URI serverUri, NetworkSettings networkSettings) { 306 assertThat(serverUri).isEqualTo(SERVER_URI); 307 return connection != null ? connection : super.connect(serverUri, networkSettings); 308 } 309 } 310 311}