001package org.w3.ldp.testsuite.test; 002 003import com.hp.hpl.jena.rdf.model.Model; 004import com.hp.hpl.jena.rdf.model.ModelFactory; 005import com.hp.hpl.jena.rdf.model.NodeIterator; 006import com.hp.hpl.jena.rdf.model.Property; 007import com.hp.hpl.jena.rdf.model.ResIterator; 008import com.hp.hpl.jena.rdf.model.Resource; 009import com.hp.hpl.jena.util.ResourceUtils; 010import com.hp.hpl.jena.vocabulary.DCTerms; 011import com.hp.hpl.jena.vocabulary.RDF; 012import com.jayway.restassured.response.Header; 013import com.jayway.restassured.response.Response; 014import com.jayway.restassured.specification.RequestSpecification; 015 016import org.apache.commons.lang3.StringUtils; 017import org.apache.marmotta.commons.vocabulary.LDP; 018import org.jboss.resteasy.plugins.delegates.LinkDelegate; 019import org.testng.annotations.AfterSuite; 020import org.testng.annotations.BeforeSuite; 021import org.testng.annotations.Optional; 022import org.testng.annotations.Parameters; 023import org.w3.ldp.testsuite.LdpTestSuite; 024import org.w3.ldp.testsuite.mapper.RdfObjectMapper; 025 026import javax.ws.rs.core.Link; 027 028import java.io.*; 029import java.net.URI; 030import java.net.URISyntaxException; 031import java.text.DateFormat; 032import java.util.ArrayList; 033import java.util.Date; 034import java.util.List; 035 036import static org.testng.Assert.assertTrue; 037import static org.w3.ldp.testsuite.http.HttpHeaders.*; 038import static org.w3.ldp.testsuite.http.LdpPreferences.PREFERENCE_INCLUDE; 039import static org.w3.ldp.testsuite.http.LdpPreferences.PREFERENCE_OMIT; 040import static org.w3.ldp.testsuite.http.MediaTypes.TEXT_TURTLE; 041import static org.w3.ldp.testsuite.matcher.HttpStatusSuccessMatcher.isSuccessful; 042 043public abstract class LdpTest { 044 045 public final static String SKIPPED_LOG_FILENAME = "skipped.log"; 046 047 public final static String HTTP_LOG_FILENAME = "http.log"; 048 public final static DateFormat df = DateFormat.getDateTimeInstance(); 049 050 public final static String DEFAULT_MODEL_TYPE = "http://example.com/ns#Bug"; 051 052 /* 053 * The following properties are marked static because commonSetup() is only called 054 * one time, even if several test classes inherit from LdpTest. 055 */ 056 057 /** 058 * Alternate content to use on POST requests 059 */ 060 private static Model postModel; 061 062 /** 063 * For HTTP details on validation failures 064 */ 065 protected static PrintWriter httpLog; 066 067 /** 068 * For skipped test logging 069 */ 070 protected static PrintWriter skipLog; 071 072 /** 073 * Builds a model from a turtle representation in a file 074 * @param path 075 */ 076 protected Model readModel(String path) { 077 Model model = null; 078 if (path != null) { 079 model = ModelFactory.createDefaultModel(); 080 InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path); 081 082 String fakeUri = "http://w3c.github.io/ldp-testsuite/fakesubject"; 083 // Even though null relative URIs are used in the resource representation file, 084 // the resulting model doesn't keep them intact. They are changed to "file://..." if 085 // an empty string is passed as base to this method. 086 model.read(inputStream, fakeUri, "TURTLE"); 087 088 // At this point, the model should contain a resource named 089 // "http://w3c.github.io/ldp-testsuite/fakesubject" if 090 // there was a null relative URI in the resource representation 091 // file. 092 Resource subject = model.getResource(fakeUri); 093 if (subject != null) { 094 ResourceUtils.renameResource(subject, ""); 095 } 096 097 try { 098 inputStream.close(); 099 } catch (IOException e) { 100 e.printStackTrace(); 101 } 102 } 103 return model; 104 } 105 106 /** 107 * Initialization of generic resource model. This will run only once 108 * at the beginning of the test suite, so postModel static field 109 * will be assigned once too. 110 * 111 * @param postTtl the resource with Turtle content to use for POST requests 112 * @param httpLogging whether to log HTTP request and response details on errors 113 */ 114 @BeforeSuite(alwaysRun = true) 115 @Parameters({"output", "postTtl", "httpLogging", "skipLogging"}) 116 public void commonSetup(@Optional String outputDir, @Optional String postTtl, @Optional String httpLogging, @Optional String skipLogging) throws IOException { 117 118 /* 119 * Note: This method is only called one time, even if many classes inherit 120 * from LdpTest. Don't set non-static members here. 121 */ 122 123 postModel = readModel(postTtl); 124 125 if (outputDir == null || outputDir.length() == 0) 126 outputDir = LdpTestSuite.OUTPUT_DIR; 127 128 File dir = new File(outputDir); 129 dir.mkdirs(); 130 131 if ("true".equals(httpLogging)) { 132 File file = new File(dir, HTTP_LOG_FILENAME); 133 try { 134 httpLog = new PrintWriter(new BufferedWriter(new FileWriter(file, true))); 135 httpLog.println(String.format("LDP Test Suite: HTTP Log (%s)", df.format(new Date()))); 136 httpLog.println("---------------------------------------------------"); 137 } catch (IOException e) { 138 System.err.println(String.format("WARNING: Error creating %s for detailed errors", HTTP_LOG_FILENAME)); 139 e.printStackTrace(); 140 } 141 } 142 143 if ("true".equals(httpLogging)) { 144 File file = new File(dir, SKIPPED_LOG_FILENAME); 145 try { 146 skipLog = new PrintWriter(new BufferedWriter(new FileWriter(file, true))); 147 skipLog.println(String.format("LDP Test Suite: Skipped Tests Log (%s)", df.format(new Date()))); 148 skipLog.println("------------------------------------------------------------"); 149 } catch (IOException e) { 150 System.err.println(String.format("WARNING: Error creating %s for detailed errors", SKIPPED_LOG_FILENAME)); 151 e.printStackTrace(); 152 } 153 } 154 155 } 156 157 @AfterSuite(alwaysRun = true) 158 public void commonTearDown() { 159 if (httpLog != null) { 160 httpLog.println(); 161 httpLog.flush(); 162 httpLog.close(); 163 } 164 if (skipLog != null) { 165 skipLog.println(); 166 skipLog.flush(); 167 skipLog.close(); 168 } 169 } 170 171 /** 172 * An absolute requirement of the specification. 173 * 174 * @see <a href="https://www.ietf.org/rfc/rfc2119.txt">RFC 2119</a> 175 */ 176 public static final String MUST = "MUST"; 177 178 /** 179 * There may exist valid reasons in particular circumstances to ignore a 180 * particular item, but the full implications must be understood and 181 * carefully weighed before choosing a different course. 182 * 183 * @see <a href="https://www.ietf.org/rfc/rfc2119.txt">RFC 2119</a> 184 */ 185 public static final String SHOULD = "SHOULD"; 186 187 /** 188 * An item is truly optional. One vendor may choose to include the item 189 * because a particular marketplace requires it or because the vendor feels 190 * that it enhances the product while another vendor may omit the same item. 191 * 192 * @see <a href="https://www.ietf.org/rfc/rfc2119.txt">RFC 2119</a> 193 */ 194 public static final String MAY = "MAY"; 195 196 /** 197 * A grouping of tests that may not need to run as part of the regular 198 * TestNG runs. Though by including it, it will allow for the generation 199 * via various reporters. 200 */ 201 public static final String MANUAL = "MANUAL"; 202 203 /** 204 * Build a base RestAssured {@link com.jayway.restassured.specification.RequestSpecification}. 205 * 206 * @return RestAssured Request Specification 207 */ 208 protected abstract RequestSpecification buildBaseRequestSpecification(); 209 210 public Model getAsModel(String uri) { 211 return getResourceAsModel(uri, TEXT_TURTLE); 212 } 213 214 public Model getResourceAsModel(String uri, String mediaType) { 215 return buildBaseRequestSpecification() 216 .header(ACCEPT, mediaType) 217 .expect() 218 .statusCode(isSuccessful()) 219 .when() 220 .get(uri).as(Model.class, new RdfObjectMapper(uri)); 221 } 222 223 protected Model getDefaultModel() { 224 Model model = ModelFactory.createDefaultModel(); 225 Resource resource = model.createResource("", 226 model.createResource(DEFAULT_MODEL_TYPE)); 227 resource.addProperty(RDF.type, model.createResource(LDP.RDFSource.stringValue())); 228 resource.addProperty( 229 model.createProperty("http://example.com/ns#severity"), "High"); 230 resource.addProperty(DCTerms.title, "Another bug to test."); 231 resource.addProperty(DCTerms.description, "Issues that need to be fixed."); 232 233 return model; 234 } 235 236 /** 237 * Given the location (URI), locate the appropriate "primary" resource within 238 * the model. Often models will have many triples with various subjects, which 239 * don't always match the request-URI. This attempts to resolve to the appropriate 240 * resource for the request-URI. This method is used to determine which subject 241 * URI should be used to assign new triples to for tests such as PUT. 242 * 243 * @param model 244 * @param location 245 * @return 246 */ 247 protected Resource getPrimaryTopic(Model model, String location) { 248 Resource loc = model.getResource(location); 249 ResIterator bugs = model.listSubjectsWithProperty(RDF.type, model.createResource(DEFAULT_MODEL_TYPE)); 250 if (bugs.hasNext()) { 251 return bugs.nextResource(); 252 } else { 253 return loc; 254 } 255 } 256 257 protected Model postContent() { 258 return postModel != null? postModel : getDefaultModel(); 259 } 260 261 /** 262 * Are there any restrictions on content when creating resources? This is 263 * assumed to be true if POST content was provided using the {@code postTtl} 264 * test parameter. 265 * 266 * <p> 267 * This method is used for 268 * {@link CommonContainerTest#testRelativeUriResolutionPost(String)}. 269 * </p> 270 * 271 * @return true if there are restrictions on what triples are allowed; false 272 * if the server allows most any RDF 273 * @see RdfSourceTest#restrictionsOnTestResourceContent() 274 */ 275 protected boolean restrictionsOnPostContent() { 276 return postModel != null; 277 } 278 279 /** 280 * Tests if a Link response header with the expected context URI, link 281 * relation, and target URI is present in an HTTP response. Resolves 282 * relative URIs against the request URI if necessary. 283 * 284 * @param linkContext 285 * the context of the Link (usually the request URI, but can be 286 * changed with an anchor parameter) 287 * @param relation 288 * the expected link relation (rel) 289 * @param linkTarget 290 * the expected URI 291 * @param requestUri 292 * the HTTP request URI (for determing a Link's context and 293 * resolving relative URIs) 294 * @param response 295 * the HTTP response 296 * @see <a href="http://tools.ietf.org/html/rfc5988">RFC 5988</a> 297 */ 298 protected boolean containsLinkHeader( 299 String linkContext, 300 String relation, 301 String linkTarget, 302 String requestUri, 303 Response response) { 304 List<Header> linkHeaders = response.getHeaders().getList(LINK); 305 for (Header linkHeader : linkHeaders) { 306 for (String s : splitLinks(linkHeader)) { 307 Link nextLink = new LinkDelegate().fromString(s); 308 if (relation.equals(nextLink.getRel())) { 309 String actualLinkUri = resolveIfRelative(requestUri, nextLink.getUri()); 310 if (linkMatchesContext(linkContext, requestUri, nextLink) && 311 linkTarget.equals(actualLinkUri)) { 312 return true; 313 } 314 } 315 } 316 } 317 318 return false; 319 } 320 321 /** 322 * Gets the first link from {@code response} with link relation {@code rel}. 323 * Resolves relative URIs against the request URI if necessary. 324 * 325 * @param linkContext 326 * the context of the Link (usually the request URI, but can be 327 * changed with an anchor parameter) 328 * @param relation 329 * the expected link relation 330 * @param requestUri 331 * the HTTP request URI (for determing a Link's context and 332 * resolving relative URIs) 333 * @param response 334 * the HTTP response 335 * @return the first link or {@code null} if none was found 336 * @see <a href="http://tools.ietf.org/html/rfc5988">RFC 5988</a> 337 */ 338 protected String getFirstLinkForRelation(String linkContext, String relation, String requestUri, Response response) { 339 List<Header> linkHeaders = response.getHeaders().getList(LINK); 340 for (Header header : linkHeaders) { 341 for (String s : splitLinks(header)) { 342 Link l = new LinkDelegate().fromString(s); 343 if (relation.equals(l.getRel()) && 344 linkMatchesContext(linkContext, requestUri, l)) { 345 return resolveIfRelative(requestUri, l.getUri()); 346 } 347 } 348 } 349 350 return null; 351 } 352 353 /** 354 * Splits an HTTP Link header that might have multiple links separated by a 355 * comma. 356 * 357 * @param linkHeader 358 * the link header 359 * @return the list of link-values as defined in RFC 5988 (for example, 360 * {@code "<http://example.com/bt/bug432>; rel=related"}) 361 * @see <a href="http://tools.ietf.org/html/rfc5988#page-7">RFC 5988: The Link Header Field</a> 362 */ 363 // LinkDelegate doesn't handle this for us 364 protected List<String> splitLinks(Header linkHeader) { 365 final ArrayList<String> links = new ArrayList<>(); 366 final String value = linkHeader.getValue(); 367 368 // Track the beginning index for the current link-value. 369 int beginIndex = 0; 370 371 // Is the current char inside a URI-Reference? 372 boolean inUriRef = false; 373 374 // Split the string on commas, but only if not in a URI-Reference 375 // delimited by angle brackets. 376 for (int i = 0; i < value.length(); ++i) { 377 final char c = value.charAt(i); 378 379 if (c == ',' && !inUriRef) { 380 // Found a comma not in a URI-Reference. Split the string. 381 final String link = value.substring(beginIndex, i).trim(); 382 links.add(link); 383 384 // Assign the next begin index for the next link. 385 beginIndex = i + 1; 386 } else if (c == '<') { 387 // Angle brackets are not legal characters in a URI, so they can 388 // only be used to mark the start and end of a URI-Reference. 389 // See http://tools.ietf.org/html/rfc3986#section-2 390 inUriRef = true; 391 } else if (c == '>') { 392 inUriRef = false; 393 } 394 } 395 396 // There should be one more link in the string. 397 final String link = value.substring(beginIndex, value.length()).trim(); 398 links.add(link); 399 400 return links; 401 } 402 403 private boolean linkMatchesContext(String expectedContext, String requestUri, Link link) { 404 String anchor = link.getParams().get("anchor"); 405 if (anchor == null) { 406 anchor = requestUri; 407 } 408 409 return anchor.equals(expectedContext); 410 } 411 412 /** 413 * Asserts the response has a <code>Preference-Applied: 414 * return=representation</code> response header, but only if at 415 * least one <code>Preference-Applied</code> header is present. 416 * 417 * @param response 418 * the HTTP response 419 */ 420 protected void checkPreferenceAppliedHeader(Response response) { 421 List<Header> preferenceAppliedHeaders = response.getHeaders().getList(PREFERNCE_APPLIED); 422 if (preferenceAppliedHeaders.isEmpty()) { 423 // The header is not mandatory. 424 return; 425 } 426 427 assertTrue(hasReturnRepresentation(preferenceAppliedHeaders), 428 "Server responded with a Preference-Applied header, but it did not contain return=representation"); 429 } 430 431 protected boolean hasReturnRepresentation(List<Header> preferenceAppliedHeaders) { 432 for (Header h : preferenceAppliedHeaders) { 433 // Handle optional whitespace, quoted preference token values, and 434 // other tokens in the Preference-Applied response header. 435 if (h.getValue().matches("(^|[ ;])return *= *\"?representation\"?($|[ ;])")) { 436 return true; 437 } 438 } 439 440 return false; 441 } 442 443 public static String include(String... preferences) { 444 return ldpPreference(PREFERENCE_INCLUDE, preferences); 445 } 446 447 public static String omit(String... preferences) { 448 return ldpPreference(PREFERENCE_OMIT, preferences); 449 } 450 451 private static String ldpPreference(String name, String... values) { 452 return "return=representation; " + name + "=\"" + StringUtils.join(values, " ") + "\""; 453 } 454 455 /** 456 * Resolves a URI if it's a relative path. 457 * 458 * @param base 459 * the base URI to use 460 * @param toResolve 461 * a URI that might be relative 462 * @return the resolved URI 463 */ 464 public static String resolveIfRelative(String base, String toResolve) { 465 try { 466 // The URI constructor accepts relative paths 467 return resolveIfRelative(base, new URI(toResolve)); 468 } catch (URISyntaxException e) { 469 throw new IllegalArgumentException(e); 470 } 471 } 472 473 protected static String resolveIfRelative(String base, URI toResolve) { 474 if (toResolve.isAbsolute()) { 475 return toResolve.toString(); 476 } 477 478 try { 479 return new URI(base).resolve(toResolve).toString(); 480 } catch (URISyntaxException e) { 481 throw new IllegalArgumentException(e); 482 } 483 } 484}