001package org.w3.ldp.testsuite.test; 002 003import com.google.common.collect.ImmutableMap; 004import com.jayway.restassured.RestAssured; 005import com.jayway.restassured.response.Header; 006import com.jayway.restassured.response.Headers; 007import com.jayway.restassured.response.Response; 008import com.jayway.restassured.specification.RequestSpecification; 009import com.jayway.restassured.specification.ResponseSpecification; 010import org.apache.commons.io.output.WriterOutputStream; 011import org.apache.commons.lang3.StringUtils; 012import org.apache.http.HttpStatus; 013import org.testng.ITestResult; 014import org.testng.annotations.*; 015import org.w3.ldp.testsuite.LdpTestSuite; 016import org.w3.ldp.testsuite.annotations.SpecTest; 017import org.w3.ldp.testsuite.annotations.SpecTest.METHOD; 018import org.w3.ldp.testsuite.annotations.SpecTest.STATUS; 019import org.w3.ldp.testsuite.exception.SkipException; 020import org.w3.ldp.testsuite.exception.SkipMethodNotAllowedException; 021import org.w3.ldp.testsuite.exception.SkipNotTestableException; 022import org.w3.ldp.testsuite.http.HttpMethod; 023import org.w3.ldp.testsuite.vocab.LDP; 024 025import java.io.IOException; 026import java.io.PrintStream; 027import java.net.URISyntaxException; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032 033import static com.jayway.restassured.config.LogConfig.logConfig; 034import static org.hamcrest.Matchers.not; 035import static org.hamcrest.Matchers.notNullValue; 036import static org.testng.Assert.assertEquals; 037import static org.testng.Assert.assertTrue; 038import static org.w3.ldp.testsuite.http.HttpHeaders.*; 039import static org.w3.ldp.testsuite.http.MediaTypes.TEXT_TURTLE; 040import static org.w3.ldp.testsuite.matcher.HeaderMatchers.isValidEntityTag; 041import static org.w3.ldp.testsuite.matcher.HttpStatusSuccessMatcher.isSuccessful; 042 043/** 044 * Common tests for all LDP resources, RDF source and non-RDF source. 045 */ 046public abstract class CommonResourceTest extends LdpTest { 047 048 private Set<String> options = new HashSet<String>(); 049 050 protected Map<String,String> auth; 051 052 protected abstract String getResourceUri(); 053 054 @BeforeClass(alwaysRun = true) 055 public void determineOptions() { 056 String uri = getResourceUri(); 057 if (StringUtils.isNotBlank(uri)) { 058 // Use HTTP OPTIONS, which MUST be supported by LDP servers, to determine what methods are supported on this container. 059 Response optionsResponse = buildBaseRequestSpecification().options(uri); 060 Headers headers = optionsResponse.getHeaders(); 061 List<Header> allowHeaders = headers.getList(ALLOW); 062 for (Header allowHeader : allowHeaders) { 063 String allow = allowHeader.getValue(); 064 if (allow != null) { 065 String[] methods = allow.split("\\s*,\\s*"); 066 for (String method : methods) { 067 options.add(method); 068 } 069 } 070 } 071 } 072 } 073 074 @AfterMethod(alwaysRun = true) 075 public void addFailureToHttpLog(ITestResult result) { 076 if (httpLog != null && result.getStatus() == ITestResult.FAILURE) { 077 // Add the failure details after the HTTP trace so it's clear what test it belongs to. 078 httpLog.println(">>> [FAILURE] Test: " + result.getName()); 079 Throwable thrown = result.getThrowable(); 080 if (thrown != null) { 081 httpLog.append(thrown.getLocalizedMessage()); 082 httpLog.println(); 083 } 084 httpLog.println(); 085 } 086 } 087 088 @Parameters("auth") 089 public CommonResourceTest(@Optional String auth) throws IOException { 090 if (StringUtils.isNotBlank(auth) && auth.contains(":")) { 091 String[] split = auth.split(":"); 092 if (split.length == 2 && StringUtils.isNotBlank(split[0]) && StringUtils.isNotBlank(split[1])) { 093 this.auth = ImmutableMap.of("username", split[0], "password", split[1]); 094 } 095 } else { 096 this.auth = null; 097 } 098 } 099 100 @Override 101 protected RequestSpecification buildBaseRequestSpecification() { 102 RequestSpecification spec = RestAssured.given(); 103 if (auth != null) { 104 spec.auth().preemptive().basic(auth.get("username"), auth.get("password")); 105 } 106 107 if (httpLog != null) { 108 spec.config(RestAssured 109 .config() 110 .logConfig(logConfig() 111 .enableLoggingOfRequestAndResponseIfValidationFails() 112 .defaultStream(new PrintStream(new WriterOutputStream(httpLog))) 113 .enablePrettyPrinting(true))); 114 } 115 116 return spec; 117 } 118 119 @Test( 120 groups = {MUST, MANUAL}, 121 description = "LDP servers MUST at least be" 122 + " HTTP/1.1 conformant servers [HTTP11].") 123 @SpecTest( 124 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-gen-http", 125 testMethod = METHOD.MANUAL, 126 approval = STATUS.WG_APPROVED, 127 comment = "Covers only part of the specification requirement. " 128 + "testIsHttp11Server covers the rest.") 129 public void testIsHttp11Manual() throws URISyntaxException { 130 throw new SkipNotTestableException(Thread.currentThread().getStackTrace()[1].getMethodName(), skipLog); 131 } 132 133 @Test( 134 groups = {MUST}, 135 description = "LDP server responses MUST use entity tags " 136 + "(either weak or strong ones) as response " 137 + "ETag header values.") 138 @SpecTest( 139 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-gen-etags", 140 testMethod = METHOD.AUTOMATED, 141 approval = STATUS.WG_APPROVED, 142 comment = "Covers only part of the specification requirement. " 143 + "testETagHeadersHead covers the rest.") 144 public void testETagHeadersGet() { 145 // GET requests 146 buildBaseRequestSpecification() 147 .expect() 148 .statusCode(isSuccessful()) 149 .header(ETAG, isValidEntityTag()) 150 .when() 151 .get(getResourceUri()); 152 } 153 154 @Test( 155 groups = {MUST}, 156 description = "LDP server responses MUST use entity tags " 157 + "(either weak or strong ones) as response " 158 + "ETag header values.") 159 @SpecTest( 160 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-gen-etags", 161 testMethod = METHOD.AUTOMATED, 162 approval = STATUS.WG_APPROVED, 163 comment = "Covers only part of the specification requirement. " 164 + "testETagHeadersGet covers the rest.") 165 public void testETagHeadersHead() { 166 // GET requests 167 buildBaseRequestSpecification() 168 .expect().statusCode(isSuccessful()).header(ETAG, isValidEntityTag()) 169 .when().head(getResourceUri()); 170 } 171 172 @Test( 173 groups = {MUST}, 174 description = "LDP servers exposing LDPRs MUST advertise " 175 + "their LDP support by exposing a HTTP Link header " 176 + "with a target URI of http://www.w3.org/ns/ldp#Resource, " 177 + "and a link relation type of type (that is, rel='type') " 178 + "in all responses to requests made to the LDPR's " 179 + "HTTP Request-URI.") 180 @SpecTest( 181 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-gen-linktypehdr", 182 testMethod = METHOD.AUTOMATED, 183 approval = STATUS.WG_APPROVED) 184 public void testLdpLinkHeader() { 185 final String uri = getResourceUri(); 186 Response response = buildBaseRequestSpecification() 187 .when() 188 .get(getResourceUri()); 189 assertTrue( 190 containsLinkHeader( 191 uri, 192 LINK_REL_TYPE, 193 LDP.Resource.stringValue(), 194 uri, 195 response 196 ), 197 "4.2.1.4 LDP servers exposing LDPRs must advertise their LDP support by exposing a HTTP Link header " 198 + "with a target URI of http://www.w3.org/ns/ldp#Resource, and a link relation type of type (that is, " 199 + "rel='type') in all responses to requests made to the LDPR's HTTP Request-URI. Actual: " 200 + response.getHeader(LINK) 201 ); 202 } 203 204 @Test( 205 groups = {MUST}, 206 description = "LDP servers MUST support the HTTP GET Method for LDPRs") 207 @SpecTest( 208 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-get-must", 209 testMethod = METHOD.AUTOMATED, 210 approval = STATUS.WG_APPROVED) 211 public void testGetResource() { 212 assertTrue(supports(HttpMethod.GET), "HTTP GET is not listed in the Allow response header on HTTP OPTIONS requests for resource <" + getResourceUri() + ">"); 213 buildBaseRequestSpecification() 214 .expect().statusCode(isSuccessful()) 215 .when().get(getResourceUri()); 216 } 217 218 @Test( 219 groups = {MUST}, 220 description = "LDP servers MUST support the HTTP response headers " 221 + "defined in section 4.2.8 HTTP OPTIONS. ") 222 @SpecTest( 223 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-get-options", 224 testMethod = METHOD.AUTOMATED, 225 approval = STATUS.WG_APPROVED) 226 public void testGetResponseHeaders() { 227 ResponseSpecification expectResponse = buildBaseRequestSpecification().expect(); 228 expectResponse.header(ALLOW, notNullValue()); 229 230 // Some headers are expected depending on OPTIONS 231 if (supports(HttpMethod.PATCH)) { 232 expectResponse.header(ACCEPT_PATCH, notNullValue()); 233 } 234 235 if (supports(HttpMethod.POST)) { 236 expectResponse.header(ACCEPT_POST, notNullValue()); 237 } 238 239 expectResponse.when().get(getResourceUri()); 240 } 241 242 243 244 245 @Test( 246 groups = {SHOULD}, 247 description = "LDP clients SHOULD use the HTTP If-Match header and HTTP ETags " 248 + "to ensure it isn’t modifying a resource that has changed since the " 249 + "client last retrieved its representation. LDP servers SHOULD require " 250 + "the HTTP If-Match header and HTTP ETags to detect collisions.") 251 @SpecTest( 252 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-put-precond", 253 testMethod = METHOD.AUTOMATED, 254 approval = STATUS.WG_APPROVED, 255 comment = "Covers only part of the specification requirement. " 256 + "testConditionFailedStatusCode, testPreconditionRequiredStatusCode " 257 + "and testPutBadETag covers the rest.") 258 public void testPutRequiresIfMatch() throws URISyntaxException { 259 skipIfMethodNotAllowed(HttpMethod.PUT); 260 261 String resourceUri = getResourceUri(); 262 Response response = buildBaseRequestSpecification() 263 .header(ACCEPT, TEXT_TURTLE) 264 .expect() 265 .statusCode(isSuccessful()) 266 .header(ETAG, isValidEntityTag()) 267 .when() 268 .get(resourceUri); 269 270 buildBaseRequestSpecification() 271 .contentType(response.getContentType()) 272 .body(response.asByteArray()) 273 .expect() 274 .statusCode(not(isSuccessful())) 275 .when() 276 .put(resourceUri); 277 } 278 279 @Test( 280 groups = {MUST}, 281 description = "LDP servers MUST respond with status code 412 " 282 + "(Condition Failed) if ETags fail to match when there " 283 + "are no other errors with the request [HTTP11]. LDP " 284 + "servers that require conditional requests MUST respond " 285 + "with status code 428 (Precondition Required) when the " 286 + "absence of a precondition is the only reason for rejecting " 287 + "the request [RFC6585].") 288 @SpecTest( 289 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-put-precond", 290 testMethod = METHOD.AUTOMATED, 291 approval = STATUS.WG_APPROVED, 292 comment = "Covers only part of the specification requirement. " 293 + "testPutBadETag, testPreconditionRequiredStatusCode " 294 + "and testPutRequiresIfMatch covers the rest.") 295 public void testConditionFailedStatusCode() { 296 skipIfMethodNotAllowed(HttpMethod.PUT); 297 298 String resourceUri = getResourceUri(); 299 Response response = buildBaseRequestSpecification() 300 .header(ACCEPT, TEXT_TURTLE) 301 .expect() 302 .statusCode(isSuccessful()).header(ETAG, isValidEntityTag()) 303 .when() 304 .get(resourceUri); 305 String contentType = response.getContentType(); 306 307 buildBaseRequestSpecification() 308 .contentType(contentType) 309 .header(IF_MATCH, "\"These aren't the ETags you're looking for.\"") 310 .body(response.asByteArray()) 311 .expect() 312 .statusCode(HttpStatus.SC_PRECONDITION_FAILED) 313 .when() 314 .put(resourceUri); 315 } 316 317 @Test( 318 groups = {MUST}, 319 description = "LDP servers MUST respond with status code 412 " 320 + "(Condition Failed) if ETags fail to match when there " 321 + "are no other errors with the request [HTTP11]. LDP " 322 + "servers that require conditional requests MUST respond " 323 + "with status code 428 (Precondition Required) when the " 324 + "absence of a precondition is the only reason for rejecting " 325 + "the request [RFC6585].") 326 @SpecTest( 327 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-put-precond", 328 testMethod = METHOD.AUTOMATED, 329 approval = STATUS.WG_APPROVED, 330 comment = "Covers only part of the specification requirement. " 331 + "testConditionFailedStatusCode, testPutBadETag" 332 + "and testPutRequiresIfMatch covers the rest.") 333 public void testPreconditionRequiredStatusCode() { 334 skipIfMethodNotAllowed(HttpMethod.PUT); 335 336 String resourceUri = getResourceUri(); 337 Response getResponse = buildBaseRequestSpecification() 338 .header(ACCEPT, TEXT_TURTLE) 339 .expect() 340 .statusCode(isSuccessful()) 341 .header(ETAG, isValidEntityTag()) 342 .when() 343 .get(resourceUri); 344 345 // Verify that we can successfully PUT the resource WITH an If-Match header. 346 Response ifMatchResponse = buildBaseRequestSpecification() 347 .header(IF_MATCH, getResponse.getHeader(ETAG)) 348 .contentType(getResponse.contentType()) 349 .body(getResponse.asByteArray()) 350 .when() 351 .put(resourceUri); 352 if (!isSuccessful().matches(ifMatchResponse.getStatusCode())) { 353 throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(), 354 "Skipping test because PUT request failed with valid If-Match header.", 355 skipLog); 356 } 357 358 // Now try WITHOUT the If-Match header. If the result is NOT successful, 359 // it should be because the header is missing and we can check the error 360 // code. 361 Response noIfMatchResponse = buildBaseRequestSpecification() 362 .contentType(getResponse.contentType()) 363 .body(getResponse.asByteArray()) 364 .when() 365 .put(resourceUri); 366 if (isSuccessful().matches(noIfMatchResponse.getStatusCode())) { 367 // It worked. This server doesn't require If-Match, which is only a 368 // SHOULD requirement (see testPutRequiresIfMatch). Skip the test. 369 throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(), 370 "Server does not require If-Match header.", skipLog); 371 } 372 373 assertEquals(428, noIfMatchResponse.getStatusCode(), "Expected 428 Precondition Required error on PUT request with no If-Match header"); 374 } 375 376 @Test( 377 groups = {MUST}, 378 description = "LDP servers MUST respond with status code 412 " 379 + "(Condition Failed) if ETags fail to match when there " 380 + "are no other errors with the request [HTTP11]. LDP " 381 + "servers that require conditional requests MUST respond " 382 + "with status code 428 (Precondition Required) when the " 383 + "absence of a precondition is the only reason for rejecting " 384 + "the request [RFC6585].") 385 @SpecTest( 386 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-put-precond", 387 testMethod = METHOD.AUTOMATED, 388 approval = STATUS.WG_APPROVED, 389 comment = "Covers only part of the specification requirement. " 390 + "testConditionFailedStatusCode, testPreconditionRequiredStatusCode " 391 + "and testPutRequiresIfMatch covers the rest.") 392 public void testPutBadETag() { 393 skipIfMethodNotAllowed(HttpMethod.PUT); 394 395 String resourceUri = getResourceUri(); 396 Response response = buildBaseRequestSpecification() 397 .header(ACCEPT, TEXT_TURTLE) 398 .expect() 399 .statusCode(isSuccessful()).header(ETAG, isValidEntityTag()) 400 .when() 401 .get(resourceUri); 402 403 buildBaseRequestSpecification() 404 .contentType(response.getContentType()) 405 .header(IF_MATCH, "\"This is not the ETag you're looking for\"") // bad ETag value 406 .body(response.asByteArray()) 407 .expect() 408 .statusCode(HttpStatus.SC_PRECONDITION_FAILED) 409 .when() 410 .put(resourceUri); 411 } 412 413 @Test( 414 groups = {MUST}, 415 description = "LDP servers MUST support the HTTP HEAD method. ") 416 @SpecTest( 417 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-head-must", 418 testMethod = METHOD.AUTOMATED, 419 approval = STATUS.WG_APPROVED) 420 public void testHead() { 421 assertTrue(supports(HttpMethod.HEAD), "HTTP HEAD is not listed in the Allow response header on HTTP OPTIONS requests for resource <" + getResourceUri() + ">"); 422 buildBaseRequestSpecification().expect().statusCode(isSuccessful()).when().head(getResourceUri()); 423 } 424 425 @Test( 426 groups = {MUST}, 427 description = "LDP servers that support PATCH MUST include an " 428 + "Accept-Patch HTTP response header [RFC5789] on HTTP " 429 + "OPTIONS requests, listing patch document media type(s) " 430 + "supported by the server. ") 431 @SpecTest( 432 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-patch-acceptpatch", 433 testMethod = METHOD.AUTOMATED, 434 approval = STATUS.WG_APPROVED) 435 public void testAcceptPatchHeader() { 436 if (supports(HttpMethod.PATCH)) { 437 buildBaseRequestSpecification() 438 .expect().statusCode(isSuccessful()).header(ACCEPT_PATCH, notNullValue()) 439 .when().options(getResourceUri()); 440 } 441 } 442 443 @Test( 444 groups = {MUST}, 445 description = "LDP servers MUST support the HTTP OPTIONS method. ") 446 @SpecTest( 447 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-options-must", 448 testMethod = METHOD.AUTOMATED, 449 approval = STATUS.WG_APPROVED) 450 public void testOptions() { 451 buildBaseRequestSpecification().expect().statusCode(isSuccessful()).when().options(getResourceUri()); 452 } 453 454 @Test( 455 groups = {MUST}, 456 description = "LDP servers MUST indicate their support for HTTP Methods " 457 + "by responding to a HTTP OPTIONS request on the LDPR’s URL " 458 + "with the HTTP Method tokens in the HTTP response header Allow. ") 459 @SpecTest( 460 specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-options-allow", 461 testMethod = METHOD.AUTOMATED, 462 approval = STATUS.WG_APPROVED) 463 public void testOptionsAllowHeader() { 464 buildBaseRequestSpecification().expect().statusCode(isSuccessful()).header(ALLOW, notNullValue()) 465 .when().options(getResourceUri()); 466 } 467 468 protected boolean supports(HttpMethod method) { 469 return options.contains(method.getName()); 470 } 471 472 protected void skipIfMethodNotAllowed(HttpMethod method) { 473 if (!supports(method)) { 474 throw new SkipMethodNotAllowedException(Thread.currentThread().getStackTrace()[1].getMethodName(), getResourceUri(), method, skipLog); 475 } 476 } 477}