From a96f07f5c19311593e4707874bdf86d85c30e7ea Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 21 Oct 2023 20:29:43 -0400 Subject: [PATCH 1/3] Support PHP 8.0 and restore CI with Github Actions --- .github/workflows/ci.yaml | 39 +++++++ .travis.yml | 17 ---- VERSION | 2 +- composer.json | 9 +- src/Favicon/Favicon.php | 5 +- tests/Favicon/FaviconTest.php | 184 ++++++++++++++++++++++++++++------ 6 files changed, 200 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/ci.yaml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..eb07b92 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,39 @@ +name: PHP CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHPCS + run: composer run-script cs + + - name: Run test suite + run: composer run-script test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cd95b9a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: php -cache: - directories: - - $HOME/.composer/cache -php: - - '5.6' - - '7.0' - - '7.1' - - '7.2' - - '7.3' - - '7.4' -install: - - composer self-update - - composer install --prefer-dist -script: - - vendor/bin/phpcs - - vendor/bin/phpunit tests diff --git a/VERSION b/VERSION index 24e56e0..6eaf894 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.2.1 \ No newline at end of file +v2.0.0 \ No newline at end of file diff --git a/composer.json b/composer.json index 53ea14b..d675c19 100644 --- a/composer.json +++ b/composer.json @@ -24,12 +24,12 @@ "sources": "https://github.com/ArthurHoaro/favicon" }, "require": { - "php": ">=5.6", + "php": "^8.0", "ext-dom": "*", "ext-fileinfo": "*" }, "require-dev": { - "phpunit/phpunit": "~4.8", + "phpunit/phpunit": "^10.0", "weew/helpers-filesystem": "~1.0", "squizlabs/php_codesniffer": "^3.5" }, @@ -37,5 +37,10 @@ "psr-4": { "Favicon\\": "src/Favicon/" } + }, + "scripts": { + "test": "phpunit", + "cs": "phpcs", + "csfix": "phpcbf" } } diff --git a/src/Favicon/Favicon.php b/src/Favicon/Favicon.php index c054e33..43a79b0 100644 --- a/src/Favicon/Favicon.php +++ b/src/Favicon/Favicon.php @@ -97,7 +97,7 @@ public function info($url) $loop = true; while ($loop && $max_loop-- > 0) { $headers = $this->dataAccess->retrieveHeader($url); - if (empty($headers) || !array_key_exists(0, $headers)) { + if (empty($headers) || !is_array($headers) || !array_key_exists(0, $headers)) { return false; } $exploded = explode(' ', $headers[0]); @@ -152,7 +152,6 @@ public function get($url = '', $type = FaviconDLType::HOTLINK_URL) $this->url = $url; } - // Get the base URL without the path for clearer concatenations. $url = rtrim($this->baseUrl($this->url, true), '/'); $original = $url; @@ -408,7 +407,7 @@ public function setUrl($url) } /** - * @param DataAccess|\PHPUnit_Framework_MockObject_MockObject $dataAccess + * @param DataAccess $dataAccess */ public function setDataAccess($dataAccess) { diff --git a/tests/Favicon/FaviconTest.php b/tests/Favicon/FaviconTest.php index 05993c3..2c0b23b 100644 --- a/tests/Favicon/FaviconTest.php +++ b/tests/Favicon/FaviconTest.php @@ -1,8 +1,9 @@ RESOURCE_FAV_ICO = self::RESOURCES . '/' . self::TEST_LOGO_NAME; $this->CACHE_TEST_DIR = self::SANDBOX; } - public function tearDown() + public function tearDown(): void { directory_delete(self::SANDBOX); } @@ -156,7 +157,7 @@ public function testBlankInfo() public function testInfoOk() { $fav = new Favicon(); - $dataAccess = $this->getMock('Favicon\DataAccess'); + $dataAccess = $this->createMock('Favicon\DataAccess'); $header = [ 0 => 'HTTP/1.1 200 OK', ]; @@ -176,42 +177,112 @@ public function testInfoOk() */ public function testInfoRedirect() { - $dataAccess = $this->getMock('Favicon\DataAccess'); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav = new Favicon(); $fav->setDataAccess($dataAccess); // Data $urlRedirect = 'http://redirected.domain.tld'; - $urlRedirect2 = 'http://redirected.domain.tld2'; $url = 'http://domain.tld'; + $headerOk = [0 => 'HTTP/1.1 200 OK']; $headerRedirect = [ 0 => 'HTTP/1.0 302 Found', 'location' => $urlRedirect, ]; - $headerOk = [0 => 'HTTP/1.1 200 OK']; // Simple redirect - $dataAccess->expects($this->at(0))->method('retrieveHeader')->will($this->returnValue($headerRedirect)); - $dataAccess->expects($this->at(1))->method('retrieveHeader')->will($this->returnValue($headerOk)); - + $dataAccess->expects($this->any())->method('retrieveHeader')->willReturnOnConsecutiveCalls( + $headerRedirect, + $headerOk, + ); $res = $fav->info($url); $this->assertEquals($urlRedirect, $res['url']); $this->assertEquals('200', $res['status']); + } + + /** + * @covers Favicon::info + * @uses Favicon + */ + public function testInfoRedirectAsArray() + { + $dataAccess = $this->createMock('Favicon\DataAccess'); + $fav = new Favicon(); + $fav->setDataAccess($dataAccess); + + // Data + $urlRedirect = 'http://redirected.domain.tld'; + $urlRedirect2 = 'http://redirected.domain.tld2'; + $url = 'http://domain.tld'; + $headerOk = [0 => 'HTTP/1.1 200 OK']; + $headerRedirect = [ + 0 => 'HTTP/1.0 302 Found', + 'location' => $urlRedirect, + ]; + $headerDoubleRedirect = array_merge($headerRedirect, ['location' => [$urlRedirect, $urlRedirect2]]); // Redirect array - $headerRedirect['location'] = [$urlRedirect, $urlRedirect2]; - $dataAccess->expects($this->at(0))->method('retrieveHeader')->will($this->returnValue($headerRedirect)); - $dataAccess->expects($this->at(1))->method('retrieveHeader')->will($this->returnValue($headerOk)); + $dataAccess->expects($this->any())->method('retrieveHeader')->willReturnOnConsecutiveCalls( + $headerDoubleRedirect, + $headerOk, + ); $res = $fav->info($url); $this->assertEquals($urlRedirect2, $res['url']); $this->assertEquals('200', $res['status']); + } + + + /** + * @covers Favicon::info + * @uses Favicon + */ + public function testInfoRedirectLoop() + { + $dataAccess = $this->createMock('Favicon\DataAccess'); + $fav = new Favicon(); + $fav->setDataAccess($dataAccess); + + // Data + $urlRedirect = 'http://redirected.domain.tld'; + $urlRedirect2 = 'http://redirected.domain.tld2'; + $url = 'http://domain.tld'; + $headerRedirect = [ + 0 => 'HTTP/1.0 302 Found', + 'location' => [$urlRedirect, $urlRedirect2], + ]; // Redirect loop - $dataAccess->expects($this->exactly(5))->method('retrieveHeader')->will($this->returnValue($headerRedirect)); + $dataAccess->expects($this->any())->method('retrieveHeader')->willReturn( + $headerRedirect, + ); $res = $fav->info($url); $this->assertEquals($urlRedirect2, $res['url']); $this->assertEquals('302', $res['status']); } + + /** + * @covers Favicon::info + * @uses Favicon + */ + public function testInfoRedirectMissingLocation() + { + $dataAccess = $this->createMock('Favicon\DataAccess'); + $fav = new Favicon(); + $fav->setDataAccess($dataAccess); + + // Data + $url = 'http://domain.tld'; + $headerRedirect = [ + 0 => 'HTTP/1.0 302 Found', + ]; + + // Redirect loop + $dataAccess->expects($this->any())->method('retrieveHeader')->willReturn( + $headerRedirect, + ); + $res = $fav->info($url); + $this->assertFalse($res); + } /** * @covers Favicon::info @@ -219,12 +290,12 @@ public function testInfoRedirect() */ public function testInfoFalse() { - $dataAccess = $this->getMock('Favicon\DataAccess'); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav = new Favicon(); $fav->setDataAccess($dataAccess); $url = 'http://domain.tld'; - $dataAccess->expects($this->at(0))->method('retrieveHeader')->will($this->returnValue(null)); + $dataAccess->expects($this->once())->method('retrieveHeader')->will($this->returnValue(null)); $this->assertFalse($fav->info($url)); } @@ -242,7 +313,7 @@ public function testGetExistingFavicon() // No cache $fav->cache(['dir' => $this->CACHE_TEST_DIR]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); // Header MOCK @@ -258,6 +329,37 @@ public function testGetExistingFavicon() ->will($this->returnCallback([$this, 'contentExistingFav'])); $this->assertEquals(self::slash($url . $path) . self::TEST_LOGO_NAME, $fav->get()); } + + /** + * @covers Favicon::get + * @uses Favicon + */ + public function testGetExistingAbsoluteFavicon() + { + $url = 'http://domain.tld/'; + $path = 'sub/'; + + $fav = new Favicon(['url' => $url . $path]); + + // No cache + $fav->cache(['dir' => $this->CACHE_TEST_DIR]); + + $dataAccess = $this->createMock('Favicon\DataAccess'); + $fav->setDataAccess($dataAccess); + + // Header MOCK + $dataAccess + ->expects($this->any()) + ->method('retrieveHeader') + ->will($this->returnCallback([$this, 'headerExistingFav'])); + + // Get from URL MOCK + $dataAccess + ->expects($this->any()) + ->method('retrieveUrl') + ->will($this->returnCallback([$this, 'contentExistingAbsoluteFav'])); + $this->assertEquals(self::slash($url) . self::TEST_LOGO_NAME, $fav->get()); + } /** * @covers Favicon::get @@ -272,7 +374,7 @@ public function testGetOriginalFavicon() // No cache $fav->cache(['dir' => $this->CACHE_TEST_DIR]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); // Header MOCK @@ -301,7 +403,7 @@ public function testGetDefaultFavicon() // No cache $fav->cache(['dir' => $this->CACHE_TEST_DIR]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); // Header MOCK @@ -329,7 +431,7 @@ public function testGetCachedFavicon() // 30s $fav->cache(['timeout' => 30, 'dir' => $this->CACHE_TEST_DIR]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createPartialMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); $fav->setDataAccess($dataAccess); // Header MOCK @@ -347,16 +449,15 @@ public function testGetCachedFavicon() $fav = new Favicon(['url' => $url]); $fav->cache(['timeout' => 30, 'dir' => $this->CACHE_TEST_DIR]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createPartialMock('Favicon\DataAccess', ['retrieveHeader']); $fav->setDataAccess($dataAccess); $dataAccess ->expects($this->any()) ->method('retrieveHeader') - ->will($this->returnValue([0 => 'HTTP/1.1 404 KO'])); - $dataAccess - ->expects($this->any()) - ->method('retrieveUrl') - ->will($this->returnValue('')); + ->willReturnOnConsecutiveCalls( + [0 => 'HTTP/1.1 404 KO'], + '' + ); $this->assertEquals(self::slash($url) . self::DEFAULT_FAV_CHECK, $fav->get()); } @@ -382,7 +483,7 @@ public function testGetNotFoundFavicon() // No cache $fav->cache(['dir' => $this->CACHE_TEST_DIR]); - $dataAccess = $this->getMock('Favicon\DataAccess'); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); $dataAccess ->expects($this->any()) @@ -407,7 +508,7 @@ public function testGetFalsePositive() // No cache $fav->cache(['dir' => $this->CACHE_TEST_DIR]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); $dataAccess ->expects($this->any()) @@ -433,7 +534,7 @@ public function testGetNoHtmlHeader() // No cache $fav->cache(['dir' => $this->CACHE_TEST_DIR]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); // MOCK @@ -458,7 +559,7 @@ public function testGetValidFavNoCacheSetup() $url = 'http://domain.tld'; $fav = new Favicon(['url' => $url]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); // MOCK @@ -484,7 +585,7 @@ public function testGetDownloadedFavPath() 'dir' => $this->CACHE_TEST_DIR ]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); // MOCK @@ -509,7 +610,7 @@ public function testGetRawImageFav() 'dir' => $this->CACHE_TEST_DIR ]); - $dataAccess = $this->getMock('Favicon\DataAccess', ['retrieveHeader', 'retrieveUrl']); + $dataAccess = $this->createMock('Favicon\DataAccess'); $fav->setDataAccess($dataAccess); // MOCK @@ -559,6 +660,23 @@ public function contentExistingFav() } return $xml; } + + /** + * Callback function for contentExistingFav in testGetExistingRootFavicon + * return valid header, or icon file content if url contain '.ico'. + * Return 200 while checking existing favicon + **/ + public function contentExistingAbsoluteFav() + { + $xml = ''; + $ico = file_get_contents($this->RESOURCE_FAV_ICO); + $args = func_get_args(); + + if (strpos($args[0], '.ico') !== false) { + return $ico; + } + return $xml; + } /** * Callback function for retrieveHeader in testGetOriginalFavicon From 6f14c4bf0d5edb6d66f185f32478a39ea857a7a0 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 21 Oct 2023 20:32:42 -0400 Subject: [PATCH 2/3] Fix CS rules --- src/Favicon/Favicon.php | 2 +- tests/Favicon/FaviconTest.php | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Favicon/Favicon.php b/src/Favicon/Favicon.php index 43a79b0..91ca3e2 100644 --- a/src/Favicon/Favicon.php +++ b/src/Favicon/Favicon.php @@ -213,7 +213,7 @@ private function getFavicon($url, $checkDefault = true) // Make sure the favicon is an absolute URL. if ($favicon && filter_var($favicon, FILTER_VALIDATE_URL) === false) { // Make sure that favicons starting with "/" get concatenated with host instead of full URL - if($favicon[0] === '/') { + if ($favicon[0] === '/') { $favicon = $this->baseUrl($url) . ltrim($favicon, '/'); } else { $favicon = rtrim($url, '/') . '/' . ltrim($favicon, '/'); diff --git a/tests/Favicon/FaviconTest.php b/tests/Favicon/FaviconTest.php index 2c0b23b..8bc3ac5 100644 --- a/tests/Favicon/FaviconTest.php +++ b/tests/Favicon/FaviconTest.php @@ -1,4 +1,5 @@ assertEquals($urlRedirect, $res['url']); $this->assertEquals('200', $res['status']); } - + /** * @covers Favicon::info * @uses Favicon @@ -230,8 +231,8 @@ public function testInfoRedirectAsArray() $this->assertEquals($urlRedirect2, $res['url']); $this->assertEquals('200', $res['status']); } - - + + /** * @covers Favicon::info * @uses Favicon @@ -259,7 +260,7 @@ public function testInfoRedirectLoop() $this->assertEquals($urlRedirect2, $res['url']); $this->assertEquals('302', $res['status']); } - + /** * @covers Favicon::info * @uses Favicon @@ -329,7 +330,7 @@ public function testGetExistingFavicon() ->will($this->returnCallback([$this, 'contentExistingFav'])); $this->assertEquals(self::slash($url . $path) . self::TEST_LOGO_NAME, $fav->get()); } - + /** * @covers Favicon::get * @uses Favicon @@ -660,7 +661,7 @@ public function contentExistingFav() } return $xml; } - + /** * Callback function for contentExistingFav in testGetExistingRootFavicon * return valid header, or icon file content if url contain '.ico'. From 51afe7c201bd97ffcdc164260c2e754f46a9dfe3 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 21 Oct 2023 20:36:29 -0400 Subject: [PATCH 3/3] Fix folder name typo --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d675c19..6dee41f 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ } }, "scripts": { - "test": "phpunit", + "test": "phpunit tests", "cs": "phpcs", "csfix": "phpcbf" }