TestConditionalRequestBuilder.java

/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */
package org.apache.hc.client5.http.impl.cache;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.apache.hc.client5.http.HeadersMatcher;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.RequestCacheControl;
import org.apache.hc.client5.http.cache.ResponseCacheControl;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicHttpRequest;
import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.hc.core5.http.support.BasicRequestBuilder;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class TestConditionalRequestBuilder {

    private ConditionalRequestBuilder<HttpRequest> impl;
    private HttpRequest request;

    @BeforeEach
    void setUp() {
        impl = new ConditionalRequestBuilder<>(request -> BasicRequestBuilder.copy(request).build());
        request = new BasicHttpRequest("GET", "/");
    }

    @Test
    void testBuildConditionalRequestWithLastModified() {
        final String theMethod = "GET";
        final String theUri = "/theuri";
        final String lastModified = "this is my last modified date";

        final HttpRequest basicRequest = new BasicHttpRequest(theMethod, theUri);
        basicRequest.addHeader("Accept-Encoding", "gzip");

        final Header[] headers = new Header[] {
                new BasicHeader("Date", DateUtils.formatStandardDate(Instant.now())),
                new BasicHeader("Last-Modified", lastModified) };

        final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry(headers);
        final ResponseCacheControl cacheControl = ResponseCacheControl.builder().build();
        final HttpRequest newRequest = impl.buildConditionalRequest(cacheControl, basicRequest, cacheEntry);

        Assertions.assertEquals(theMethod, newRequest.getMethod());
        Assertions.assertEquals(theUri, newRequest.getRequestUri());
        Assertions.assertEquals(2, newRequest.getHeaders().length);

        MatcherAssert.assertThat(
                newRequest.getHeaders(),
                HeadersMatcher.same(
                        new BasicHeader("Accept-Encoding", "gzip"),
                        new BasicHeader("If-Modified-Since", lastModified)));
    }

    @Test
    void testConditionalRequestForEntryWithLastModifiedAndEtagIncludesBothAsValidators() {
        final Instant now = Instant.now();
        final Instant tenSecondsAgo = now.minusSeconds(10);
        final Instant twentySecondsAgo = now.plusSeconds(20);
        final String lmDate = DateUtils.formatStandardDate(twentySecondsAgo);
        final String etag = "\"etag\"";
        final Header[] headers = {
            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
            new BasicHeader("Last-Modified", lmDate),
            new BasicHeader("ETag", etag)
        };
        final HttpRequest basicRequest = new BasicHttpRequest("GET", "/");
        final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry(headers);
        final ResponseCacheControl cacheControl = ResponseCacheControl.builder().build();
        final HttpRequest result = impl.buildConditionalRequest(cacheControl, basicRequest, cacheEntry);
        Assertions.assertEquals(lmDate,
                result.getFirstHeader("If-Modified-Since").getValue());
        Assertions.assertEquals(etag,
                result.getFirstHeader("If-None-Match").getValue());
    }

    @Test
    void testBuildConditionalRequestWithETag() {
        final String theMethod = "GET";
        final String theUri = "/theuri";
        final String theETag = "\"this is my eTag\"";

        final HttpRequest basicRequest = new BasicHttpRequest(theMethod, theUri);
        basicRequest.addHeader("Accept-Encoding", "gzip");

        final Instant now = Instant.now();

        final Header[] headers = new Header[] {
                new BasicHeader("Date", DateUtils.formatStandardDate(now)),
                new BasicHeader("Last-Modified", DateUtils.formatStandardDate(now)),
                new BasicHeader("ETag", theETag) };

        final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry(headers);

        final ResponseCacheControl cacheControl = ResponseCacheControl.builder().build();
        final HttpRequest newRequest = impl.buildConditionalRequest(cacheControl, basicRequest, cacheEntry);

        Assertions.assertEquals(theMethod, newRequest.getMethod());
        Assertions.assertEquals(theUri, newRequest.getRequestUri());

        MatcherAssert.assertThat(
                newRequest.getHeaders(),
                HeadersMatcher.same(
                        new BasicHeader("Accept-Encoding", "gzip"),
                        new BasicHeader("If-None-Match", theETag),
                        new BasicHeader("If-Modified-Since", DateUtils.formatStandardDate(now))));
    }

    @Test
    void testCacheEntryWithMustRevalidateDoesEndToEndRevalidation() {
        final HttpRequest basicRequest = new BasicHttpRequest("GET","/");
        final Instant now = Instant.now();
        final Instant elevenSecondsAgo = now.minusSeconds(11);
        final Instant tenSecondsAgo = now.minusSeconds(10);
        final Instant nineSecondsAgo = now.plusSeconds(9);

        final Header[] cacheEntryHeaders = new Header[] {
                new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
                new BasicHeader("ETag", "\"etag\"")
        };
        final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo, cacheEntryHeaders);

        final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
                .setMaxAge(5)
                .setMustRevalidate(true)
                .build();
        final HttpRequest result = impl.buildConditionalRequest(responseCacheControl, basicRequest, cacheEntry);

        final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(result);
        Assertions.assertEquals(0, requestCacheControl.getMaxAge());
    }

    @Test
    void testCacheEntryWithProxyRevalidateDoesEndToEndRevalidation() {
        final HttpRequest basicRequest = new BasicHttpRequest("GET", "/");
        final Instant now = Instant.now();
        final Instant elevenSecondsAgo = now.minusSeconds(11);
        final Instant tenSecondsAgo = now.minusSeconds(10);
        final Instant nineSecondsAgo = now.plusSeconds(9);

        final Header[] cacheEntryHeaders = new Header[] {
                new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
                new BasicHeader("ETag", "\"etag\"")
        };
        final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo, cacheEntryHeaders);

        final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
                .setMaxAge(5)
                .setProxyRevalidate(true)
                .build();
        final HttpRequest result = impl.buildConditionalRequest(responseCacheControl, basicRequest, cacheEntry);

        final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(result);
        Assertions.assertEquals(0, requestCacheControl.getMaxAge());
    }

    @Test
    void testBuildUnconditionalRequestUsesGETMethod() {
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        Assertions.assertEquals("GET", result.getMethod());
    }

    @Test
    void testBuildUnconditionalRequestUsesRequestUri() {
        final String uri = "/theURI";
        request = new BasicHttpRequest("GET", uri);
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        Assertions.assertEquals(uri, result.getRequestUri());
    }

    @Test
    void testBuildUnconditionalRequestAddsCacheControlNoCache() {
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(result);
        Assertions.assertTrue(requestCacheControl.isNoCache());
    }

    @Test
    void testBuildUnconditionalRequestDoesNotUseIfRange() {
        request.addHeader("If-Range","\"etag\"");
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        Assertions.assertNull(result.getFirstHeader("If-Range"));
    }

    @Test
    void testBuildUnconditionalRequestDoesNotUseIfMatch() {
        request.addHeader("If-Match","\"etag\"");
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        Assertions.assertNull(result.getFirstHeader("If-Match"));
    }

    @Test
    void testBuildUnconditionalRequestDoesNotUseIfNoneMatch() {
        request.addHeader("If-None-Match","\"etag\"");
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        Assertions.assertNull(result.getFirstHeader("If-None-Match"));
    }

    @Test
    void testBuildUnconditionalRequestDoesNotUseIfUnmodifiedSince() {
        request.addHeader("If-Unmodified-Since", DateUtils.formatStandardDate(Instant.now()));
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        Assertions.assertNull(result.getFirstHeader("If-Unmodified-Since"));
    }

    @Test
    void testBuildUnconditionalRequestDoesNotUseIfModifiedSince() {
        request.addHeader("If-Modified-Since", DateUtils.formatStandardDate(Instant.now()));
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        Assertions.assertNull(result.getFirstHeader("If-Modified-Since"));
    }

    @Test
    void testBuildUnconditionalRequestCarriesOtherRequestHeaders() {
        request.addHeader("User-Agent","MyBrowser/1.0");
        final HttpRequest result = impl.buildUnconditionalRequest(request);
        Assertions.assertEquals("MyBrowser/1.0",
                result.getFirstHeader("User-Agent").getValue());
    }

    @Test
    void testBuildConditionalRequestFromVariants() {
        final ETag etag1 = new ETag("123");
        final ETag etag2 = new ETag("456");
        final ETag etag3 = new ETag("789");

        final List<ETag> variantEntries = Arrays.asList(etag1, etag2, etag3);

        final HttpRequest conditional = impl.buildConditionalRequestFromVariants(request, variantEntries);


        final Iterator<String> it = MessageSupport.iterateTokens(conditional, HttpHeaders.IF_NONE_MATCH);
        final List<ETag> etags = new ArrayList<>();
        while (it.hasNext()) {
            etags.add(ETag.parse(it.next()));
        }
        MatcherAssert.assertThat(etags, Matchers.containsInAnyOrder(etag1, etag2, etag3));
    }

}