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}