'''
Test the ESI plugin.
'''
#  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.

import os

Test.Summary = '''
Test the ESI plugin.
'''

Test.SkipUnless(
    Condition.PluginExists('esi.so'),
)


class EsiTest():
    """
    A class that encapsulates the configuration and execution of a set of EPI
    test cases.
    """

    """ static: The same server Process is used across all tests. """
    _server = None

    """ static: A counter to keep the ATS process names unique across tests. """
    _ts_counter = 0

    """ static: A counter to keep any output file names unique across tests. """
    _output_counter = 0

    """ The ATS process for this set of test cases. """
    _ts = None

    def __init__(self, plugin_config):
        """
        Args:
            plugin_config (str): The config line to place in plugin.config for
                the ATS process.
        """
        if EsiTest._server is None:
            EsiTest._server = EsiTest._create_server()

        self._ts = EsiTest._create_ats(self, plugin_config)

    @staticmethod
    def _create_server():
        """
        Create and start a server process.
        """
        # Configure our server.
        server = Test.MakeOriginServer("server")

        # Generate the set of ESI responses derived right from our ESI docs.
        # See:
        #   doc/admin-guide/plugins/esi.en.rst
        request_header = {
            "headers": (
                "GET /esi.php HTTP/1.1\r\n"
                "Host: www.example.com\r\n"
                "Content-Length: 0\r\n\r\n"),
            "timestamp": "1469733493.993",
            "body": ""
        }
        esi_body = r'''<?php   header('X-Esi: 1'); ?>
<html>
<body>
Hello, <esi:include src="http://www.example.com/date.php"/>
</body>
</html>
'''
        response_header = {
            "headers": (
                "HTTP/1.1 200 OK\r\n"
                "Content-Type: text/html\r\n"
                "X-Esi: 1\r\n"
                "Connection: close\r\n"
                "Content-Length: {}\r\n"
                "Cache-Control: max-age=300\r\n"
                "\r\n".format(len(esi_body))),
            "timestamp": "1469733493.993",
            "body": esi_body
        }
        server.addResponse("sessionfile.log", request_header, response_header)
        request_header = {
            "headers": (
                "GET /date.php HTTP/1.1\r\n"
                "Host: www.example.com\r\n"
                "Content-Length: 0\r\n\r\n"),
            "timestamp": "1469733493.993",
            "body": ""
        }
        date_body = r'''<?php
header ("Cache-control: no-cache");
echo date('l jS \of F Y h:i:s A');
?>
'''
        response_header = {
            "headers": (
                "HTTP/1.1 200 OK\r\n"
                "Content-Type: text/html\r\n"
                "Connection: close\r\n"
                "Content-Length: {}\r\n"
                "Cache-Control: max-age=300\r\n"
                "\r\n".format(len(date_body))),
            "timestamp": "1469733493.993",
            "body": date_body
        }
        server.addResponse("sessionfile.log", request_header, response_header)
        # Verify correct functionality with an empty body.
        request_header = {
            "headers": (
                "GET /expect_empty_body HTTP/1.1\r\n"
                "Host: www.example.com\r\n"
                "Content-Length: 0\r\n\r\n"),
            "timestamp": "1469733493.993",
            "body": ""
        }
        response_header = {
            "headers": (
                "HTTP/1.1 200 OK\r\n"
                "X-ESI: On\r\n"
                "Content-Length: 0\r\n"
                "Connection: close\r\n"
                "Content-Type: text/html; charset=UTF-8\r\n"
                "\r\n"),
            "timestamp": "1469733493.993",
            "body": ""
        }
        server.addResponse("sessionfile.log", request_header, response_header)

        # Create a run to start the server.
        tr = Test.AddTestRun("Start the server.")
        tr.Processes.Default.StartBefore(server)
        tr.Processes.Default.Command = "echo starting the server"
        tr.Processes.Default.ReturnCode = 0
        tr.StillRunningAfter = server

        return server

    @staticmethod
    def _create_ats(self, plugin_config):
        """
        Create and start an ATS process.
        """
        EsiTest._ts_counter += 1

        # Configure ATS with a vanilla ESI plugin configuration.
        ts = Test.MakeATSProcess("ts{}".format(EsiTest._ts_counter))
        ts.Disk.records_config.update({
            'proxy.config.diags.debug.enabled': 1,
            'proxy.config.diags.debug.tags': 'http|plugin_esi',
        })
        ts.Disk.remap_config.AddLine(
            'map http://www.example.com/ http://127.0.0.1:{0}'.format(EsiTest._server.Variables.Port)
        )
        ts.Disk.plugin_config.AddLine(plugin_config)

        # Create a run to start the ATS process.
        tr = Test.AddTestRun("Start the ATS process.")
        tr.Processes.Default.StartBefore(ts)
        tr.Processes.Default.Command = "echo starting ATS"
        tr.Processes.Default.ReturnCode = 0
        tr.StillRunningAfter = ts
        return ts

    def run_cases_expecting_gzip(self):
        # Test 1: Verify basic ESI functionality.
        tr = Test.AddTestRun("First request for esi.php: not cached")
        tr.Processes.Default.Command = \
            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
             '-H"Accept: */*" --verbose'.format(
                 self._ts.Variables.port))
        tr.Processes.Default.ReturnCode = 0
        tr.Processes.Default.Streams.stderr = "gold/esi_headers.gold"
        tr.Processes.Default.Streams.stdout = "gold/esi_body.gold"
        tr.StillRunningAfter = self._server
        tr.StillRunningAfter = self._ts

        # Test 2: Repeat the above, should now be cached.
        tr = Test.AddTestRun("Second request for esi.php: will be cached")
        tr.Processes.Default.Command = \
            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
             '-H"Accept: */*" --verbose'.format(
                 self._ts.Variables.port))
        tr.Processes.Default.ReturnCode = 0
        tr.Processes.Default.Streams.stderr = "gold/esi_headers.gold"
        tr.Processes.Default.Streams.stdout = "gold/esi_body.gold"
        tr.StillRunningAfter = self._server
        tr.StillRunningAfter = self._ts

        # Test 3: Verify the ESI plugin can gzip a response when the client accepts it.
        tr = Test.AddTestRun("Verify the ESI plugin can gzip a response")
        EsiTest._output_counter += 1
        unzipped_body_file = os.path.join(
                tr.RunDirectory,
                "non_empty_curl_output_{}".format(EsiTest._output_counter))
        gzipped_body_file = unzipped_body_file + ".gz"
        tr.Processes.Default.Command = \
            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
             '-H "Accept-Encoding: gzip" -H"Accept: */*" --verbose --output {1}'.format(
                 self._ts.Variables.port, gzipped_body_file))
        tr.Processes.Default.ReturnCode = 0
        tr.Processes.Default.Ready = When.FileExists(gzipped_body_file)
        tr.Processes.Default.Streams.stderr = "gold/esi_gzipped.gold"
        tr.StillRunningAfter = self._server
        tr.StillRunningAfter = self._ts
        zipped_body_disk_file = tr.Disk.File(gzipped_body_file)
        zipped_body_disk_file.Exists = True

        # Now, unzip the file and make sure its size is the expected body.
        tr = Test.AddTestRun("Verify the file uzips to the expected body.")
        tr.Processes.Default.Command = "gunzip {}".format(gzipped_body_file)
        tr.Processes.Default.Ready = When.FileExists(unzipped_body_file)
        tr.Processes.Default.ReturnCode = 0
        unzipped_body_disk_file = tr.Disk.File(unzipped_body_file)
        unzipped_body_disk_file.Content = "gold/esi_body.gold"

        # Test 4: Verify correct handling of a gzipped empty response body.
        tr = Test.AddTestRun("Verify we can handle an empty response.")
        EsiTest._output_counter += 1
        empty_body_file = os.path.join(
                tr.RunDirectory,
                "empty_curl_output_{}".format(EsiTest._output_counter))
        gzipped_empty_body = empty_body_file + ".gz"
        tr.Processes.Default.Command = \
            ('curl http://127.0.0.1:{0}/expect_empty_body -H"Host: www.example.com" '
             '-H"Accept-Encoding: gzip" -H"Accept: */*" --verbose --output {1}'.format(
                 self._ts.Variables.port, gzipped_empty_body))
        tr.Processes.Default.ReturnCode = 0
        tr.Processes.Default.Ready = When.FileExists(gzipped_empty_body)
        tr.Processes.Default.Streams.stderr = "gold/empty_response_body.gold"
        tr.StillRunningAfter = self._server
        tr.StillRunningAfter = self._ts
        # The gzipped output file should be greater than 0, even though 0 bytes are
        # compressed.
        gz_disk_file = tr.Disk.File(gzipped_empty_body)
        gz_disk_file.Size = Testers.GreaterThan(0)

        # Now, unzip the file and make sure its size is the original 0 size body.
        tr = Test.AddTestRun("Verify the file uzips to a zero sized file.")
        tr.Processes.Default.Command = "gunzip {}".format(gzipped_empty_body)
        tr.Processes.Default.Ready = When.FileExists(empty_body_file)
        tr.Processes.Default.ReturnCode = 0
        unzipped_disk_file = tr.Disk.File(empty_body_file)
        unzipped_disk_file.Size = 0

    def run_cases_expecting_no_gzip(self):
        # Test 1: Run an ESI test where the client does not accept gzip.
        tr = Test.AddTestRun("First request for esi.php: gzip not accepted.")
        tr.Processes.Default.Command = \
            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
             '-H"Accept: */*" --verbose'.format(
                 self._ts.Variables.port))
        tr.Processes.Default.ReturnCode = 0
        tr.Processes.Default.Streams.stderr = "gold/esi_headers.gold"
        tr.Processes.Default.Streams.stdout = "gold/esi_body.gold"
        tr.StillRunningAfter = self._server
        tr.StillRunningAfter = self._ts

        # Test 2: Verify the ESI plugin does not gzip the response even if the
        # client accepts the gzip encoding.
        tr = Test.AddTestRun("Verify the ESI plugin refuses to gzip responses with -–disable-gzip-output")
        tr.Processes.Default.Command = \
            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
             '-H "Accept-Encoding: gzip" -H"Accept: */*" --verbose'.format(
                 self._ts.Variables.port))
        tr.Processes.Default.ReturnCode = 0
        tr.Processes.Default.Streams.stderr = "gold/esi_headers.gold"
        tr.Processes.Default.Streams.stdout = "gold/esi_body.gold"
        tr.StillRunningAfter = self._server
        tr.StillRunningAfter = self._ts


#
# Configure and run the test cases.
#

# Run the tests with ESI configured with no parameters.
vanilla_test = EsiTest(plugin_config='esi.so')
vanilla_test.run_cases_expecting_gzip()

# For these test cases, the behavior should remain the same with
# --first-byte-flush set.
first_byte_flush_test = EsiTest(plugin_config='esi.so --first-byte-flush')
first_byte_flush_test.run_cases_expecting_gzip()

# For these test cases, the behavior should remain the same with
# --packed-node-support set.
packed_node_support_test = EsiTest(plugin_config='esi.so --packed-node-support')
packed_node_support_test.run_cases_expecting_gzip()

# Run a set of cases verifying that the plugin does not zip content if
# --disable-gzip-output is set.
gzip_disabled_test = EsiTest(plugin_config='esi.so --disable-gzip-output')
gzip_disabled_test.run_cases_expecting_no_gzip()
