# -*- coding: utf-8 -*-
"""
test_header_indexing.py
~~~~~~~~~~~~~~~~~~~~~~~

This module contains tests that use HPACK header tuples that provide additional
metadata to the hpack module about how to encode the headers.
"""
import pytest

from jh2.hpack import HeaderTuple, NeverIndexedHeaderTuple

import jh2.config
import jh2.connection


def assert_header_blocks_actually_equal(block_a, block_b):
    """
    Asserts that two header bocks are really, truly equal, down to the types
    of their tuples. Doesn't return anything.
    """
    assert len(block_a) == len(block_b)

    for a, b in zip(block_a, block_b):
        assert a == b
        assert a.__class__ is b.__class__


class TestHeaderIndexing(object):
    """
    Test that Hyper-h2 can correctly handle never indexed header fields using
    the appropriate hpack data structures.
    """
    example_request_headers = [
        HeaderTuple(u':authority', u'example.com'),
        HeaderTuple(u':path', u'/'),
        HeaderTuple(u':scheme', u'https'),
        HeaderTuple(u':method', u'GET'),
    ]
    bytes_example_request_headers = [
        HeaderTuple(b':authority', b'example.com'),
        HeaderTuple(b':path', b'/'),
        HeaderTuple(b':scheme', b'https'),
        HeaderTuple(b':method', b'GET'),
    ]

    extended_request_headers = [
        HeaderTuple(u':authority', u'example.com'),
        HeaderTuple(u':path', u'/'),
        HeaderTuple(u':scheme', u'https'),
        HeaderTuple(u':method', u'GET'),
        NeverIndexedHeaderTuple(u'authorization', u'realpassword'),
    ]
    bytes_extended_request_headers = [
        HeaderTuple(b':authority', b'example.com'),
        HeaderTuple(b':path', b'/'),
        HeaderTuple(b':scheme', b'https'),
        HeaderTuple(b':method', b'GET'),
        NeverIndexedHeaderTuple(b'authorization', b'realpassword'),
    ]

    example_response_headers = [
        HeaderTuple(u':status', u'200'),
        HeaderTuple(u'server', u'fake-serv/0.1.0')
    ]
    bytes_example_response_headers = [
        HeaderTuple(b':status', b'200'),
        HeaderTuple(b'server', b'fake-serv/0.1.0')
    ]

    extended_response_headers = [
        HeaderTuple(u':status', u'200'),
        HeaderTuple(u'server', u'fake-serv/0.1.0'),
        NeverIndexedHeaderTuple(u'secure', u'you-bet'),
    ]
    bytes_extended_response_headers = [
        HeaderTuple(b':status', b'200'),
        HeaderTuple(b'server', b'fake-serv/0.1.0'),
        NeverIndexedHeaderTuple(b'secure', b'you-bet'),
    ]

    server_config = jh2.config.H2Configuration(client_side=False)

    @pytest.mark.parametrize(
        'headers', (
            example_request_headers,
            bytes_example_request_headers,
            extended_request_headers,
            bytes_extended_request_headers,
        )
    )
    def test_sending_header_tuples(self, headers, frame_factory):
        """
        Providing HeaderTuple and HeaderTuple subclasses preserves the metadata
        about indexing.
        """
        c = jh2.connection.H2Connection()
        c.initiate_connection()

        # Clear the data, then send headers.
        c.clear_outbound_data_buffer()
        c.send_headers(1, headers)

        f = frame_factory.build_headers_frame(headers=headers)
        assert c.data_to_send() == f.serialize()

    @pytest.mark.parametrize(
        'headers', (
            example_request_headers,
            bytes_example_request_headers,
            extended_request_headers,
            bytes_extended_request_headers,
        )
    )
    def test_header_tuples_in_pushes(self, headers, frame_factory):
        """
        Providing HeaderTuple and HeaderTuple subclasses to push promises
        preserves metadata about indexing.
        """
        c = jh2.connection.H2Connection(config=self.server_config)
        c.receive_data(frame_factory.preamble())

        # We can use normal headers for the request.
        f = frame_factory.build_headers_frame(
            self.example_request_headers
        )
        c.receive_data(f.serialize())

        frame_factory.refresh_encoder()
        expected_frame = frame_factory.build_push_promise_frame(
            stream_id=1,
            promised_stream_id=2,
            headers=headers,
            flags=['END_HEADERS'],
        )

        c.clear_outbound_data_buffer()
        c.push_stream(
            stream_id=1,
            promised_stream_id=2,
            request_headers=headers
        )

        assert c.data_to_send() == expected_frame.serialize()

    @pytest.mark.parametrize(
        'headers,encoding', (
            (example_request_headers, 'utf-8'),
            (bytes_example_request_headers, None),
            (extended_request_headers, 'utf-8'),
            (bytes_extended_request_headers, None),
        )
    )
    def test_header_tuples_are_decoded_request(self,
                                               headers,
                                               encoding,
                                               frame_factory):
        """
        The indexing status of the header is preserved when emitting
        RequestReceived events.
        """
        config = jh2.config.H2Configuration(
            client_side=False, header_encoding=encoding
        )
        c = jh2.connection.H2Connection(config=config)
        c.receive_data(frame_factory.preamble())

        f = frame_factory.build_headers_frame(headers)
        data = f.serialize()
        events = c.receive_data(data)

        assert len(events) == 1
        event = events[0]

        assert isinstance(event, jh2.events.RequestReceived)
        assert_header_blocks_actually_equal(headers, event.headers)

    @pytest.mark.parametrize(
        'headers,encoding', (
            (example_response_headers, 'utf-8'),
            (bytes_example_response_headers, None),
            (extended_response_headers, 'utf-8'),
            (bytes_extended_response_headers, None),
        )
    )
    def test_header_tuples_are_decoded_response(self,
                                                headers,
                                                encoding,
                                                frame_factory):
        """
        The indexing status of the header is preserved when emitting
        ResponseReceived events.
        """
        config = jh2.config.H2Configuration(
            header_encoding=encoding
        )
        c = jh2.connection.H2Connection(config=config)
        c.initiate_connection()
        c.send_headers(stream_id=1, headers=self.example_request_headers)

        f = frame_factory.build_headers_frame(headers)
        data = f.serialize()
        events = c.receive_data(data)

        assert len(events) == 1
        event = events[0]

        assert isinstance(event, jh2.events.ResponseReceived)
        assert_header_blocks_actually_equal(headers, event.headers)

    @pytest.mark.parametrize(
        'headers,encoding', (
            (example_response_headers, 'utf-8'),
            (bytes_example_response_headers, None),
            (extended_response_headers, 'utf-8'),
            (bytes_extended_response_headers, None),
        )
    )
    def test_header_tuples_are_decoded_info_response(self,
                                                     headers,
                                                     encoding,
                                                     frame_factory):
        """
        The indexing status of the header is preserved when emitting
        InformationalResponseReceived events.
        """
        # Manipulate the headers to send 100 Continue. We need to copy the list
        # to avoid breaking the example headers.
        headers = headers[:]
        if encoding:
            headers[0] = HeaderTuple(u':status', u'100')
        else:
            headers[0] = HeaderTuple(b':status', b'100')

        config = jh2.config.H2Configuration(
            header_encoding=encoding
        )
        c = jh2.connection.H2Connection(config=config)
        c.initiate_connection()
        c.send_headers(stream_id=1, headers=self.example_request_headers)

        f = frame_factory.build_headers_frame(headers)
        data = f.serialize()
        events = c.receive_data(data)

        assert len(events) == 1
        event = events[0]

        assert isinstance(event, jh2.events.InformationalResponseReceived)
        assert_header_blocks_actually_equal(headers, event.headers)

    @pytest.mark.parametrize(
        'headers,encoding', (
            (example_response_headers, 'utf-8'),
            (bytes_example_response_headers, None),
            (extended_response_headers, 'utf-8'),
            (bytes_extended_response_headers, None),
        )
    )
    def test_header_tuples_are_decoded_trailers(self,
                                                headers,
                                                encoding,
                                                frame_factory):
        """
        The indexing status of the header is preserved when emitting
        TrailersReceived events.
        """
        # Manipulate the headers to remove the status, which shouldn't be in
        # the trailers. We need to copy the list to avoid breaking the example
        # headers.
        headers = headers[1:]

        config = jh2.config.H2Configuration(
            header_encoding=encoding
        )
        c = jh2.connection.H2Connection(config=config)
        c.initiate_connection()
        c.send_headers(stream_id=1, headers=self.example_request_headers)
        f = frame_factory.build_headers_frame(self.example_response_headers)
        data = f.serialize()
        c.receive_data(data)

        f = frame_factory.build_headers_frame(headers, flags=['END_STREAM'])
        data = f.serialize()
        events = c.receive_data(data)

        assert len(events) == 2
        event = events[0]

        assert isinstance(event, jh2.events.TrailersReceived)
        assert_header_blocks_actually_equal(headers, event.headers)

    @pytest.mark.parametrize(
        'headers,encoding', (
            (example_request_headers, 'utf-8'),
            (bytes_example_request_headers, None),
            (extended_request_headers, 'utf-8'),
            (bytes_extended_request_headers, None),
        )
    )
    def test_header_tuples_are_decoded_push_promise(self,
                                                    headers,
                                                    encoding,
                                                    frame_factory):
        """
        The indexing status of the header is preserved when emitting
        PushedStreamReceived events.
        """
        config = jh2.config.H2Configuration(
            header_encoding=encoding
        )
        c = jh2.connection.H2Connection(config=config)
        c.initiate_connection()
        c.send_headers(stream_id=1, headers=self.example_request_headers)

        f = frame_factory.build_push_promise_frame(
            stream_id=1,
            promised_stream_id=2,
            headers=headers,
            flags=['END_HEADERS'],
        )
        data = f.serialize()
        events = c.receive_data(data)

        assert len(events) == 1
        event = events[0]

        assert isinstance(event, jh2.events.PushedStreamReceived)
        assert_header_blocks_actually_equal(headers, event.headers)


class TestSecureHeaders(object):
    """
    Certain headers should always be transformed to their never-indexed form.
    """
    example_request_headers = [
        (u':authority', u'example.com'),
        (u':path', u'/'),
        (u':scheme', u'https'),
        (u':method', u'GET'),
    ]
    bytes_example_request_headers = [
        (b':authority', b'example.com'),
        (b':path', b'/'),
        (b':scheme', b'https'),
        (b':method', b'GET'),
    ]
    possible_auth_headers = [
        (u'authorization', u'test'),
        (u'Authorization', u'test'),
        (u'authorization', u'really long test'),
        HeaderTuple(u'authorization', u'test'),
        HeaderTuple(u'Authorization', u'test'),
        HeaderTuple(u'authorization', u'really long test'),
        NeverIndexedHeaderTuple(u'authorization', u'test'),
        NeverIndexedHeaderTuple(u'Authorization', u'test'),
        NeverIndexedHeaderTuple(u'authorization', u'really long test'),
        (b'authorization', b'test'),
        (b'Authorization', b'test'),
        (b'authorization', b'really long test'),
        HeaderTuple(b'authorization', b'test'),
        HeaderTuple(b'Authorization', b'test'),
        HeaderTuple(b'authorization', b'really long test'),
        NeverIndexedHeaderTuple(b'authorization', b'test'),
        NeverIndexedHeaderTuple(b'Authorization', b'test'),
        NeverIndexedHeaderTuple(b'authorization', b'really long test'),
        (u'proxy-authorization', u'test'),
        (u'Proxy-Authorization', u'test'),
        (u'proxy-authorization', u'really long test'),
        HeaderTuple(u'proxy-authorization', u'test'),
        HeaderTuple(u'Proxy-Authorization', u'test'),
        HeaderTuple(u'proxy-authorization', u'really long test'),
        NeverIndexedHeaderTuple(u'proxy-authorization', u'test'),
        NeverIndexedHeaderTuple(u'Proxy-Authorization', u'test'),
        NeverIndexedHeaderTuple(u'proxy-authorization', u'really long test'),
        (b'proxy-authorization', b'test'),
        (b'Proxy-Authorization', b'test'),
        (b'proxy-authorization', b'really long test'),
        HeaderTuple(b'proxy-authorization', b'test'),
        HeaderTuple(b'Proxy-Authorization', b'test'),
        HeaderTuple(b'proxy-authorization', b'really long test'),
        NeverIndexedHeaderTuple(b'proxy-authorization', b'test'),
        NeverIndexedHeaderTuple(b'Proxy-Authorization', b'test'),
        NeverIndexedHeaderTuple(b'proxy-authorization', b'really long test'),
    ]
    secured_cookie_headers = [
        (u'cookie', u'short'),
        (u'Cookie', u'short'),
        (u'cookie', u'nineteen byte cooki'),
        HeaderTuple(u'cookie', u'short'),
        HeaderTuple(u'Cookie', u'short'),
        HeaderTuple(u'cookie', u'nineteen byte cooki'),
        NeverIndexedHeaderTuple(u'cookie', u'short'),
        NeverIndexedHeaderTuple(u'Cookie', u'short'),
        NeverIndexedHeaderTuple(u'cookie', u'nineteen byte cooki'),
        NeverIndexedHeaderTuple(u'cookie', u'longer manually secured cookie'),
        (b'cookie', b'short'),
        (b'Cookie', b'short'),
        (b'cookie', b'nineteen byte cooki'),
        HeaderTuple(b'cookie', b'short'),
        HeaderTuple(b'Cookie', b'short'),
        HeaderTuple(b'cookie', b'nineteen byte cooki'),
        NeverIndexedHeaderTuple(b'cookie', b'short'),
        NeverIndexedHeaderTuple(b'Cookie', b'short'),
        NeverIndexedHeaderTuple(b'cookie', b'nineteen byte cooki'),
        NeverIndexedHeaderTuple(b'cookie', b'longer manually secured cookie'),
    ]
    unsecured_cookie_headers = [
        (u'cookie', u'twenty byte cookie!!'),
        (u'Cookie', u'twenty byte cookie!!'),
        (u'cookie', u'substantially longer than 20 byte cookie'),
        HeaderTuple(u'cookie', u'twenty byte cookie!!'),
        HeaderTuple(u'cookie', u'twenty byte cookie!!'),
        HeaderTuple(u'Cookie', u'twenty byte cookie!!'),
        (b'cookie', b'twenty byte cookie!!'),
        (b'Cookie', b'twenty byte cookie!!'),
        (b'cookie', b'substantially longer than 20 byte cookie'),
        HeaderTuple(b'cookie', b'twenty byte cookie!!'),
        HeaderTuple(b'cookie', b'twenty byte cookie!!'),
        HeaderTuple(b'Cookie', b'twenty byte cookie!!'),
    ]

    server_config = jh2.config.H2Configuration(client_side=False)

    @pytest.mark.parametrize(
        'headers', (example_request_headers, bytes_example_request_headers)
    )
    @pytest.mark.parametrize('auth_header', possible_auth_headers)
    def test_authorization_headers_never_indexed(self,
                                                 headers,
                                                 auth_header,
                                                 frame_factory):
        """
        Authorization and Proxy-Authorization headers are always forced to be
        never-indexed, regardless of their form.
        """
        # Regardless of what we send, we expect it to be never indexed.
        send_headers = headers + [auth_header]
        expected_headers = headers + [
            NeverIndexedHeaderTuple(auth_header[0].lower(), auth_header[1])
        ]

        c = jh2.connection.H2Connection()
        c.initiate_connection()

        # Clear the data, then send headers.
        c.clear_outbound_data_buffer()
        c.send_headers(1, send_headers)

        f = frame_factory.build_headers_frame(headers=expected_headers)
        assert c.data_to_send() == f.serialize()

    @pytest.mark.parametrize(
        'headers', (example_request_headers, bytes_example_request_headers)
    )
    @pytest.mark.parametrize('auth_header', possible_auth_headers)
    def test_authorization_headers_never_indexed_push(self,
                                                      headers,
                                                      auth_header,
                                                      frame_factory):
        """
        Authorization and Proxy-Authorization headers are always forced to be
        never-indexed, regardless of their form, when pushed by a server.
        """
        # Regardless of what we send, we expect it to be never indexed.
        send_headers = headers + [auth_header]
        expected_headers = headers + [
            NeverIndexedHeaderTuple(auth_header[0].lower(), auth_header[1])
        ]

        c = jh2.connection.H2Connection(config=self.server_config)
        c.receive_data(frame_factory.preamble())

        # We can use normal headers for the request.
        f = frame_factory.build_headers_frame(
            self.example_request_headers
        )
        c.receive_data(f.serialize())

        frame_factory.refresh_encoder()
        expected_frame = frame_factory.build_push_promise_frame(
            stream_id=1,
            promised_stream_id=2,
            headers=expected_headers,
            flags=['END_HEADERS'],
        )

        c.clear_outbound_data_buffer()
        c.push_stream(
            stream_id=1,
            promised_stream_id=2,
            request_headers=send_headers
        )

        assert c.data_to_send() == expected_frame.serialize()

    @pytest.mark.parametrize(
        'headers', (example_request_headers, bytes_example_request_headers)
    )
    @pytest.mark.parametrize('cookie_header', secured_cookie_headers)
    def test_short_cookie_headers_never_indexed(self,
                                                headers,
                                                cookie_header,
                                                frame_factory):
        """
        Short cookie headers, and cookies provided as NeverIndexedHeaderTuple,
        are never indexed.
        """
        # Regardless of what we send, we expect it to be never indexed.
        send_headers = headers + [cookie_header]
        expected_headers = headers + [
            NeverIndexedHeaderTuple(cookie_header[0].lower(), cookie_header[1])
        ]

        c = jh2.connection.H2Connection()
        c.initiate_connection()

        # Clear the data, then send headers.
        c.clear_outbound_data_buffer()
        c.send_headers(1, send_headers)

        f = frame_factory.build_headers_frame(headers=expected_headers)
        assert c.data_to_send() == f.serialize()

    @pytest.mark.parametrize(
        'headers', (example_request_headers, bytes_example_request_headers)
    )
    @pytest.mark.parametrize('cookie_header', secured_cookie_headers)
    def test_short_cookie_headers_never_indexed_push(self,
                                                     headers,
                                                     cookie_header,
                                                     frame_factory):
        """
        Short cookie headers, and cookies provided as NeverIndexedHeaderTuple,
        are never indexed when pushed by servers.
        """
        # Regardless of what we send, we expect it to be never indexed.
        send_headers = headers + [cookie_header]
        expected_headers = headers + [
            NeverIndexedHeaderTuple(cookie_header[0].lower(), cookie_header[1])
        ]

        c = jh2.connection.H2Connection(config=self.server_config)
        c.receive_data(frame_factory.preamble())

        # We can use normal headers for the request.
        f = frame_factory.build_headers_frame(
            self.example_request_headers
        )
        c.receive_data(f.serialize())

        frame_factory.refresh_encoder()
        expected_frame = frame_factory.build_push_promise_frame(
            stream_id=1,
            promised_stream_id=2,
            headers=expected_headers,
            flags=['END_HEADERS'],
        )

        c.clear_outbound_data_buffer()
        c.push_stream(
            stream_id=1,
            promised_stream_id=2,
            request_headers=send_headers
        )

        assert c.data_to_send() == expected_frame.serialize()

    @pytest.mark.parametrize(
        'headers', (example_request_headers, bytes_example_request_headers)
    )
    @pytest.mark.parametrize('cookie_header', unsecured_cookie_headers)
    def test_long_cookie_headers_can_be_indexed(self,
                                                headers,
                                                cookie_header,
                                                frame_factory):
        """
        Longer cookie headers can be indexed.
        """
        # Regardless of what we send, we expect it to be indexed.
        send_headers = headers + [cookie_header]
        expected_headers = headers + [
            HeaderTuple(cookie_header[0].lower(), cookie_header[1])
        ]

        c = jh2.connection.H2Connection()
        c.initiate_connection()

        # Clear the data, then send headers.
        c.clear_outbound_data_buffer()
        c.send_headers(1, send_headers)

        f = frame_factory.build_headers_frame(headers=expected_headers)
        assert c.data_to_send() == f.serialize()

    @pytest.mark.parametrize(
        'headers', (example_request_headers, bytes_example_request_headers)
    )
    @pytest.mark.parametrize('cookie_header', unsecured_cookie_headers)
    def test_long_cookie_headers_can_be_indexed_push(self,
                                                     headers,
                                                     cookie_header,
                                                     frame_factory):
        """
        Longer cookie headers can be indexed.
        """
        # Regardless of what we send, we expect it to be never indexed.
        send_headers = headers + [cookie_header]
        expected_headers = headers + [
            HeaderTuple(cookie_header[0].lower(), cookie_header[1])
        ]

        c = jh2.connection.H2Connection(config=self.server_config)
        c.receive_data(frame_factory.preamble())

        # We can use normal headers for the request.
        f = frame_factory.build_headers_frame(
            self.example_request_headers
        )
        c.receive_data(f.serialize())

        frame_factory.refresh_encoder()
        expected_frame = frame_factory.build_push_promise_frame(
            stream_id=1,
            promised_stream_id=2,
            headers=expected_headers,
            flags=['END_HEADERS'],
        )

        c.clear_outbound_data_buffer()
        c.push_stream(
            stream_id=1,
            promised_stream_id=2,
            request_headers=send_headers
        )

        assert c.data_to_send() == expected_frame.serialize()
