Index: /trunk/tests/phpunit/tests/http/wpHttpEncoding.php
===================================================================
--- /trunk/tests/phpunit/tests/http/wpHttpEncoding.php	(revision 64411)
+++ /trunk/tests/phpunit/tests/http/wpHttpEncoding.php	(revision 64411)
@@ -0,0 +1,414 @@
+<?php
+
+/**
+ * Tests for the WP_Http_Encoding class.
+ *
+ * @group http
+ *
+ * @coversDefaultClass WP_Http_Encoding
+ */
+class Tests_HTTP_wpHttpEncoding extends WP_UnitTestCase {
+
+	const CONTENT = 'The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.';
+
+	/**
+	 * Builds a gzip stream with the given header flags and optional header fields.
+	 *
+	 * When the FHCRC flag is set, a valid header checksum is appended after the optional fields.
+	 *
+	 * @param int    $flags  Gzip header flags.
+	 * @param string $fields Optional header fields that follow the fixed ten-byte header.
+	 * @return string Gzip stream.
+	 */
+	private static function build_gzip_stream( $flags, $fields ) {
+		$header = "\x1f\x8b\x08" . chr( $flags ) . "\x00\x00\x00\x00\x00\x03" . $fields;
+
+		if ( $flags & 2 ) {
+			// The header checksum is the low 16 bits of the CRC-32 of the preceding header bytes.
+			$header .= pack( 'v', crc32( $header ) & 0xffff );
+		}
+
+		$trailer = pack( 'V', crc32( self::CONTENT ) ) . pack( 'V', strlen( self::CONTENT ) );
+
+		return $header . gzdeflate( self::CONTENT ) . $trailer;
+	}
+
+	/**
+	 * Tests that compress() returns raw deflate data.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::compress
+	 *
+	 * @requires function gzdeflate
+	 * @requires function gzinflate
+	 */
+	public function test_compress_returns_raw_deflate_data() {
+		$compressed = WP_Http_Encoding::compress( self::CONTENT );
+
+		$this->assertNotSame( self::CONTENT, $compressed, 'The content should be compressed.' );
+		$this->assertLessThan( strlen( self::CONTENT ), strlen( $compressed ), 'The compressed content should be shorter.' );
+		$this->assertSame( self::CONTENT, gzinflate( $compressed ), 'The compressed content should be raw deflate data.' );
+	}
+
+	/**
+	 * Tests that compress() respects the compression level.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::compress
+	 *
+	 * @requires function gzdeflate
+	 * @requires function gzinflate
+	 */
+	public function test_compress_uses_compression_level() {
+		$stored = WP_Http_Encoding::compress( self::CONTENT, 0 );
+
+		$this->assertStringContainsString( self::CONTENT, $stored, 'Level 0 should store the content without compressing it.' );
+		$this->assertSame( self::CONTENT, gzinflate( $stored ), 'The stored content should be raw deflate data.' );
+	}
+
+	/**
+	 * Tests that decompress() handles each supported compression format.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::decompress
+	 * @covers ::compatible_gzinflate
+	 *
+	 * @dataProvider data_compressed_content
+	 *
+	 * @requires function gzdeflate
+	 * @requires function gzinflate
+	 * @requires function gzcompress
+	 * @requires function gzuncompress
+	 * @requires function gzencode
+	 * @requires function gzdecode
+	 *
+	 * @param callable $encoder Function that compresses the content.
+	 * @param array    $args    Arguments passed to the function.
+	 */
+	public function test_decompress( $encoder, $args ) {
+		$compressed = call_user_func_array( $encoder, $args );
+
+		$this->assertSame( self::CONTENT, WP_Http_Encoding::decompress( $compressed ) );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * The compressed content is generated in the test, after its required functions are checked.
+	 *
+	 * @return array[]
+	 */
+	public static function data_compressed_content() {
+		return array(
+			'raw deflate'                     => array( 'gzdeflate', array( self::CONTENT ) ),
+			'raw deflate without compression' => array( 'gzdeflate', array( self::CONTENT, 0 ) ),
+			'zlib'                            => array( 'gzcompress', array( self::CONTENT ) ),
+			'gzip'                            => array( 'gzencode', array( self::CONTENT ) ),
+			'gzip with a file name'           => array( array( self::class, 'build_gzip_stream' ), array( 8, "content.txt\0" ) ),
+			'WP_Http_Encoding::compress()'    => array( array( 'WP_Http_Encoding', 'compress' ), array( self::CONTENT ) ),
+		);
+	}
+
+	/**
+	 * Tests that decompress() returns content it cannot decompress unchanged.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::decompress
+	 *
+	 * @dataProvider data_content_that_is_not_compressed
+	 *
+	 * @requires function gzinflate
+	 * @requires function gzuncompress
+	 *
+	 * @param mixed $content Content.
+	 */
+	public function test_decompress_returns_uncompressed_content_unchanged( $content ) {
+		$this->assertSame( $content, WP_Http_Encoding::decompress( $content ) );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array[]
+	 */
+	public static function data_content_that_is_not_compressed() {
+		return array(
+			'plain text'   => array( self::CONTENT ),
+			'HTML'         => array( '<!DOCTYPE html><html><body>Hello</body></html>' ),
+			'empty string' => array( '' ),
+			'zero string'  => array( '0' ),
+			'false'        => array( false ),
+			'null'         => array( null ),
+		);
+	}
+
+	/**
+	 * Tests that compatible_gzinflate() skips the optional gzip header fields.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::compatible_gzinflate
+	 *
+	 * @dataProvider data_gzip_streams
+	 *
+	 * @requires function gzdeflate
+	 * @requires function gzinflate
+	 * @requires function gzcompress
+	 * @requires function gzencode
+	 *
+	 * @param callable $encoder Function that builds the stream.
+	 * @param array    $args    Arguments passed to the function.
+	 */
+	public function test_compatible_gzinflate_decodes_gzip_streams( $encoder, $args ) {
+		$gz_data = call_user_func_array( $encoder, $args );
+
+		$this->assertSame( self::CONTENT, WP_Http_Encoding::compatible_gzinflate( $gz_data ) );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * The streams are built in the test, after its required functions are checked.
+	 *
+	 * @return array[]
+	 */
+	public static function data_gzip_streams() {
+		$build = array( self::class, 'build_gzip_stream' );
+
+		return array(
+			'gzencode()'                             => array( 'gzencode', array( self::CONTENT ) ),
+			'no optional fields'                     => array( $build, array( 0, '' ) ),
+			'header checksum'                        => array( $build, array( 2, '' ) ),
+			'file name'                              => array( $build, array( 8, "content.txt\0" ) ),
+			'comment'                                => array( $build, array( 16, "A comment\0" ) ),
+			'file name, comment and header checksum' => array( $build, array( 26, "content.txt\0" . "A comment\0" ) ),
+			'zlib stream with a two-byte header'     => array( 'gzcompress', array( self::CONTENT ) ),
+		);
+	}
+
+	/**
+	 * Tests that compatible_gzinflate() returns false for data it cannot decode.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::compatible_gzinflate
+	 *
+	 * @dataProvider data_invalid_gzip_streams
+	 *
+	 * @requires function gzinflate
+	 *
+	 * @param string $gz_data Data.
+	 */
+	public function test_compatible_gzinflate_returns_false_for_invalid_data( $gz_data ) {
+		$this->assertFalse( WP_Http_Encoding::compatible_gzinflate( $gz_data ) );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array[]
+	 */
+	public static function data_invalid_gzip_streams() {
+		return array(
+			'plain text'                  => array( self::CONTENT ),
+			'empty string'                => array( '' ),
+			'gzip header without content' => array( "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03" ),
+		);
+	}
+
+	/**
+	 * Tests the Accept-Encoding header value for different request arguments.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::accept_encoding
+	 *
+	 * @dataProvider data_accept_encoding
+	 *
+	 * @requires function gzinflate
+	 * @requires function gzuncompress
+	 * @requires function gzdecode
+	 *
+	 * @param array  $args     Request arguments.
+	 * @param string $expected Expected header value.
+	 */
+	public function test_accept_encoding( $args, $expected ) {
+		$this->assertSame( $expected, WP_Http_Encoding::accept_encoding( 'http://example.org/', $args ) );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array[]
+	 */
+	public static function data_accept_encoding() {
+		$all = 'deflate;q=1.0, compress;q=0.5, gzip;q=0.5';
+
+		return array(
+			'decompression enabled'         => array(
+				array(
+					'decompress' => true,
+					'stream'     => false,
+				),
+				$all,
+			),
+			'limit_response_size is null'   => array(
+				array(
+					'decompress'          => true,
+					'stream'              => false,
+					'limit_response_size' => null,
+				),
+				$all,
+			),
+			'decompression disabled'        => array(
+				array(
+					'decompress' => false,
+					'stream'     => false,
+				),
+				'',
+			),
+			'streaming to a file'           => array(
+				array(
+					'decompress' => true,
+					'stream'     => true,
+				),
+				'',
+			),
+			'limited response size'         => array(
+				array(
+					'decompress'          => true,
+					'stream'              => false,
+					'limit_response_size' => 1024,
+				),
+				'',
+			),
+			'response size limited to zero' => array(
+				array(
+					'decompress'          => true,
+					'stream'              => false,
+					'limit_response_size' => 0,
+				),
+				'',
+			),
+		);
+	}
+
+	/**
+	 * Tests that the accepted encodings are filterable.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::accept_encoding
+	 *
+	 * @requires function gzinflate
+	 * @requires function gzuncompress
+	 * @requires function gzdecode
+	 */
+	public function test_accept_encoding_is_filterable() {
+		$args = array(
+			'decompress' => true,
+			'stream'     => false,
+		);
+
+		$filter = new MockAction();
+		add_filter( 'wp_http_accept_encoding', array( $filter, 'filter' ), 10, 3 );
+		add_filter(
+			'wp_http_accept_encoding',
+			static function ( $type ) {
+				return array( $type[0], 'br;q=0.9' );
+			},
+			20
+		);
+
+		$this->assertSame( 'deflate;q=1.0, br;q=0.9', WP_Http_Encoding::accept_encoding( 'http://example.org/path', $args ), 'The filtered encodings should be used.' );
+		$this->assertSame(
+			array(
+				array(
+					array( 'deflate;q=1.0', 'compress;q=0.5', 'gzip;q=0.5' ),
+					'http://example.org/path',
+					$args,
+				),
+			),
+			$filter->get_args(),
+			'The filter should receive the encodings, the URL and the request arguments.'
+		);
+	}
+
+	/**
+	 * Tests the content encoding used for compressed request bodies.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::content_encoding
+	 */
+	public function test_content_encoding() {
+		$this->assertSame( 'deflate', WP_Http_Encoding::content_encoding() );
+	}
+
+	/**
+	 * Tests whether response headers indicate an encoded body.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::should_decode
+	 *
+	 * @dataProvider data_should_decode
+	 *
+	 * @param mixed $headers  Response headers.
+	 * @param bool  $expected Whether the body should be decoded.
+	 */
+	public function test_should_decode( $headers, $expected ) {
+		$this->assertSame( $expected, WP_Http_Encoding::should_decode( $headers ) );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array[]
+	 */
+	public static function data_should_decode() {
+		return array(
+			'array with content-encoding'          => array( array( 'content-encoding' => 'gzip' ), true ),
+			'array with other headers'             => array(
+				array(
+					'content-type'     => 'text/html',
+					'content-encoding' => 'deflate',
+				),
+				true,
+			),
+			'array without content-encoding'       => array( array( 'content-type' => 'text/html' ), false ),
+			'array with empty content-encoding'    => array( array( 'content-encoding' => '' ), false ),
+			'array with null content-encoding'     => array( array( 'content-encoding' => null ), false ),
+			'array with zero content-encoding'     => array( array( 'content-encoding' => '0' ), false ),
+			'array with capitalized header name'   => array( array( 'Content-Encoding' => 'gzip' ), false ),
+			'empty array'                          => array( array(), false ),
+			'string with content-encoding'         => array( "Content-Type: text/html\r\nContent-Encoding: gzip\r\n", true ),
+			'string with lowercase header name'    => array( 'content-encoding: gzip', true ),
+			'string with uppercase header name'    => array( 'CONTENT-ENCODING: gzip', true ),
+			'string without content-encoding'      => array( "Content-Type: text/html\r\n", false ),
+			'string with header name but no colon' => array( 'content-encoding', false ),
+			'empty string'                         => array( '', false ),
+			'null'                                 => array( null, false ),
+			'false'                                => array( false, false ),
+			'true'                                 => array( true, false ),
+			'integer'                              => array( 1, false ),
+		);
+	}
+
+	/**
+	 * Tests that compression is available when the zlib functions exist.
+	 *
+	 * @ticket 65819
+	 *
+	 * @covers ::is_available
+	 *
+	 * @requires function gzinflate
+	 */
+	public function test_is_available() {
+		$this->assertTrue( WP_Http_Encoding::is_available() );
+	}
+}
