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.*; 018import static org.junit.jupiter.api.Assertions.assertThrows; 019import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp; 020import static org.shredzone.acme4j.toolbox.TestUtils.getJSON; 021import static org.shredzone.acme4j.toolbox.TestUtils.url; 022 023import java.io.IOException; 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.assertThatExceptionOfType(AcmeNotSupportedException.class) 121 .isThrownBy(order::getProfile); 122 softly.assertThat(order.getLocation()).isEqualTo(locationUrl); 123 softly.assertThat(order.getAuthorizations()).isNotNull(); 124 softly.assertThat(order.getAuthorizations()).hasSize(2); 125 } 126 127 provider.close(); 128 } 129 130 /** 131 * Test that a new auto-renewal {@link Order} can be created. 132 */ 133 @Test 134 public void testAutoRenewOrderCertificate() throws Exception { 135 var autoRenewStart = parseTimestamp("2018-01-01T00:00:00Z"); 136 var autoRenewEnd = parseTimestamp("2019-01-01T00:00:00Z"); 137 var validity = Duration.ofDays(7); 138 var predate = Duration.ofDays(6); 139 140 var provider = new TestableConnectionProvider() { 141 @Override 142 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 143 assertThat(url).isEqualTo(resourceUrl); 144 assertThatJson(claims.toString()).isEqualTo(getJSON("requestAutoRenewOrderRequest").toString()); 145 assertThat(login).isNotNull(); 146 return HttpURLConnection.HTTP_CREATED; 147 } 148 149 @Override 150 public JSON readJsonResponse() { 151 return getJSON("requestAutoRenewOrderResponse"); 152 } 153 154 @Override 155 public URL getLocation() { 156 return locationUrl; 157 } 158 }; 159 160 var login = provider.createLogin(); 161 162 provider.putMetadata("auto-renewal",JSON.parse( 163 "{\"allow-certificate-get\": true}" 164 ).toMap()); 165 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 166 167 var account = new Account(login); 168 var order = account.newOrder() 169 .domain("example.org") 170 .autoRenewal() 171 .autoRenewalStart(autoRenewStart) 172 .autoRenewalEnd(autoRenewEnd) 173 .autoRenewalLifetime(validity) 174 .autoRenewalLifetimeAdjust(predate) 175 .autoRenewalEnableGet() 176 .create(); 177 178 try (var softly = new AutoCloseableSoftAssertions()) { 179 softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder(Identifier.dns("example.org")); 180 softly.assertThat(order.getNotBefore()).isEmpty(); 181 softly.assertThat(order.getNotAfter()).isEmpty(); 182 softly.assertThat(order.isAutoRenewing()).isTrue(); 183 softly.assertThat(order.getAutoRenewalStartDate().orElseThrow()).isEqualTo(autoRenewStart); 184 softly.assertThat(order.getAutoRenewalEndDate()).isEqualTo(autoRenewEnd); 185 softly.assertThat(order.getAutoRenewalLifetime()).isEqualTo(validity); 186 softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow()).isEqualTo(predate); 187 softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue(); 188 softly.assertThat(order.getLocation()).isEqualTo(locationUrl); 189 } 190 191 provider.close(); 192 } 193 194 /** 195 * Test that a new {@link Order} with ancestor domain can be created. 196 */ 197 @Test 198 public void testOrderCertificateWithAncestor() throws Exception { 199 var notBefore = parseTimestamp("2016-01-01T00:00:00Z"); 200 var notAfter = parseTimestamp("2016-01-08T00:00:00Z"); 201 202 var provider = new TestableConnectionProvider() { 203 @Override 204 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 205 assertThat(url).isEqualTo(resourceUrl); 206 assertThatJson(claims.toString()).isEqualTo(getJSON("requestOrderRequestSub").toString()); 207 assertThat(login).isNotNull(); 208 return HttpURLConnection.HTTP_CREATED; 209 } 210 211 @Override 212 public JSON readJsonResponse() { 213 return getJSON("requestOrderResponseSub"); 214 } 215 216 @Override 217 public URL getLocation() { 218 return locationUrl; 219 } 220 }; 221 222 var login = provider.createLogin(); 223 224 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 225 provider.putMetadata("subdomainAuthAllowed", true); 226 227 var account = new Account(login); 228 var order = account.newOrder() 229 .identifier(Identifier.dns("foo.bar.example.com").withAncestorDomain("example.com")) 230 .notBefore(notBefore) 231 .notAfter(notAfter) 232 .create(); 233 234 try (var softly = new AutoCloseableSoftAssertions()) { 235 softly.assertThat(order.getIdentifiers()).containsExactlyInAnyOrder( 236 Identifier.dns("foo.bar.example.com")); 237 softly.assertThat(order.getNotBefore().orElseThrow()) 238 .isEqualTo("2016-01-01T00:10:00Z"); 239 softly.assertThat(order.getNotAfter().orElseThrow()) 240 .isEqualTo("2016-01-08T00:10:00Z"); 241 softly.assertThat(order.getExpires().orElseThrow()) 242 .isEqualTo("2016-01-10T00:00:00Z"); 243 softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING); 244 softly.assertThat(order.getLocation()).isEqualTo(locationUrl); 245 softly.assertThat(order.getAuthorizations()).isNotNull(); 246 softly.assertThat(order.getAuthorizations()).hasSize(2); 247 } 248 249 provider.close(); 250 } 251 252 /** 253 * Test that a new {@link Order} with ancestor domain fails if not supported. 254 */ 255 @Test 256 public void testOrderCertificateWithAncestorFails() throws Exception { 257 var provider = new TestableConnectionProvider(); 258 259 var login = provider.createLogin(); 260 261 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 262 263 assertThat(login.getSession().getMetadata().isSubdomainAuthAllowed()).isFalse(); 264 265 var account = new Account(login); 266 assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> 267 account.newOrder() 268 .identifier(Identifier.dns("foo.bar.example.com").withAncestorDomain("example.com")) 269 .create() 270 ); 271 272 provider.close(); 273 } 274 275 /** 276 * Test that an auto-renewal {@link Order} cannot be created if unsupported by the CA. 277 */ 278 @Test 279 public void testAutoRenewOrderCertificateFails() { 280 assertThrows(AcmeNotSupportedException.class, () -> { 281 var provider = new TestableConnectionProvider(); 282 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 283 284 var login = provider.createLogin(); 285 286 var account = new Account(login); 287 account.newOrder() 288 .domain("example.org") 289 .autoRenewal() 290 .create(); 291 292 provider.close(); 293 }); 294 } 295 296 /** 297 * Test that auto-renew and notBefore/notAfter cannot be mixed. 298 */ 299 @Test 300 public void testAutoRenewNotMixed() throws Exception { 301 var someInstant = parseTimestamp("2018-01-01T00:00:00Z"); 302 303 var provider = new TestableConnectionProvider(); 304 var login = provider.createLogin(); 305 306 var account = new Account(login); 307 308 assertThrows(IllegalArgumentException.class, () -> { 309 OrderBuilder ob = account.newOrder().autoRenewal(); 310 ob.notBefore(someInstant); 311 }, "accepted notBefore"); 312 313 assertThrows(IllegalArgumentException.class, () -> { 314 OrderBuilder ob = account.newOrder().autoRenewal(); 315 ob.notAfter(someInstant); 316 }, "accepted notAfter"); 317 318 assertThrows(IllegalArgumentException.class, () -> { 319 OrderBuilder ob = account.newOrder().notBefore(someInstant); 320 ob.autoRenewal(); 321 }, "accepted autoRenewal"); 322 323 assertThrows(IllegalArgumentException.class, () -> { 324 OrderBuilder ob = account.newOrder().notBefore(someInstant); 325 ob.autoRenewalStart(someInstant); 326 }, "accepted autoRenewalStart"); 327 328 assertThrows(IllegalArgumentException.class, () -> { 329 OrderBuilder ob = account.newOrder().notBefore(someInstant); 330 ob.autoRenewalEnd(someInstant); 331 }, "accepted autoRenewalEnd"); 332 333 assertThrows(IllegalArgumentException.class, () -> { 334 OrderBuilder ob = account.newOrder().notBefore(someInstant); 335 ob.autoRenewalLifetime(Duration.ofDays(7)); 336 }, "accepted autoRenewalLifetime"); 337 338 provider.close(); 339 } 340 341 /** 342 * Test that a new profile {@link Order} can be created. 343 */ 344 @Test 345 public void testProfileOrderCertificate() throws Exception { 346 var provider = new TestableConnectionProvider() { 347 @Override 348 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 349 assertThat(url).isEqualTo(resourceUrl); 350 assertThatJson(claims.toString()).isEqualTo(getJSON("requestProfileOrderRequest").toString()); 351 assertThat(login).isNotNull(); 352 return HttpURLConnection.HTTP_CREATED; 353 } 354 355 @Override 356 public JSON readJsonResponse() { 357 return getJSON("requestProfileOrderResponse"); 358 } 359 360 @Override 361 public URL getLocation() { 362 return locationUrl; 363 } 364 }; 365 366 var login = provider.createLogin(); 367 368 provider.putMetadata("profiles",JSON.parse( 369 "{\"classic\": \"The same profile you're accustomed to\"}" 370 ).toMap()); 371 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 372 373 var account = new Account(login); 374 var order = account.newOrder() 375 .domain("example.org") 376 .profile("classic") 377 .create(); 378 379 try (var softly = new AutoCloseableSoftAssertions()) { 380 softly.assertThat(order.getProfile()).isEqualTo("classic"); 381 } 382 383 provider.close(); 384 } 385 386 /** 387 * Test that a profile {@link Order} cannot be created if the profile is unsupported 388 * by the CA. 389 */ 390 @Test 391 public void testUnsupportedProfileOrderCertificateFails() throws Exception { 392 var provider = new TestableConnectionProvider(); 393 provider.putMetadata("profiles",JSON.parse( 394 "{\"classic\": \"The same profile you're accustomed to\"}" 395 ).toMap()); 396 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 397 398 var login = provider.createLogin(); 399 400 var account = new Account(login); 401 assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> { 402 account.newOrder() 403 .domain("example.org") 404 .profile("invalid") 405 .create(); 406 }).withMessage("Server does not support profile: invalid"); 407 provider.close(); 408 } 409 410 /** 411 * Test that a profile {@link Order} cannot be created if the feature is unsupported 412 * by the CA. 413 */ 414 @Test 415 public void testProfileOrderCertificateFails() throws IOException { 416 var provider = new TestableConnectionProvider(); 417 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 418 419 var login = provider.createLogin(); 420 421 var account = new Account(login); 422 assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> { 423 account.newOrder() 424 .domain("example.org") 425 .profile("classic") 426 .create(); 427 }).withMessage("Server does not support profile"); 428 429 provider.close(); 430 } 431 432 /** 433 * Test that the ARI replaces field is set. 434 */ 435 @Test 436 public void testARIReplaces() throws Exception { 437 var provider = new TestableConnectionProvider() { 438 @Override 439 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 440 assertThat(url).isEqualTo(resourceUrl); 441 assertThatJson(claims.toString()).isEqualTo(getJSON("requestReplacesRequest").toString()); 442 assertThat(login).isNotNull(); 443 return HttpURLConnection.HTTP_CREATED; 444 } 445 446 @Override 447 public JSON readJsonResponse() { 448 return getJSON("requestReplacesResponse"); 449 } 450 451 @Override 452 public URL getLocation() { 453 return locationUrl; 454 } 455 }; 456 457 var login = provider.createLogin(); 458 459 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 460 provider.putTestResource(Resource.RENEWAL_INFO, resourceUrl); 461 462 var account = new Account(login); 463 account.newOrder() 464 .domain("example.org") 465 .replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE") 466 .create(); 467 468 provider.close(); 469 } 470 471 /** 472 * Test that exception is thrown if the ARI replaces field is set but ARI is not 473 * supported. 474 */ 475 @Test 476 public void testARIReplaceFails() throws Exception { 477 var provider = new TestableConnectionProvider() { 478 @Override 479 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 480 fail("Request was sent"); 481 return HttpURLConnection.HTTP_FORBIDDEN; 482 } 483 }; 484 485 var login = provider.createLogin(); 486 487 provider.putTestResource(Resource.NEW_ORDER, resourceUrl); 488 489 var account = new Account(login); 490 assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> { 491 account.newOrder() 492 .domain("example.org") 493 .replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE") 494 .create(); 495 }) 496 .withMessage("Server does not support renewal-information"); 497 498 provider.close(); 499 } 500 501}