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}