Merge pull request #24578 from nextcloud/add-integration-tests-for-avatars

This commit is contained in:
John Molakvoæ 2020-12-07 09:16:52 +01:00 committed by GitHub
commit 76ffc46c01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 499 additions and 9 deletions

View file

@ -857,6 +857,31 @@ trigger:
- pull_request
- push
---
kind: pipeline
name: integration-avatar
steps:
- name: submodules
image: docker:git
commands:
- git submodule update --init
- name: integration-auth
image: nextcloudci/integration-php7.3:integration-php7.3-2
commands:
- bash tests/drone-run-integration-tests.sh || exit 0
- ./occ maintenance:install --admin-pass=admin --data-dir=/dev/shm/nc_int
- cd build/integration
- ./run.sh features/avatar.feature
trigger:
branch:
- master
- stable*
event:
- pull_request
- push
---
kind: pipeline
name: integration-maintenance-mode

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1,183 @@
Feature: avatar
Background:
Given user "user0" exists
Scenario: get default user avatar
When user "user0" gets avatar for user "user0"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
And last avatar is a square of size 128
And last avatar is not a single color
Scenario: get default user avatar as an anonymous user
When user "anonymous" gets avatar for user "user0"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
And last avatar is a square of size 128
And last avatar is not a single color
Scenario: get temporary user avatar before cropping it
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/green-square-256.png"
When logged in user gets temporary avatar
Then The following headers should be set
| Content-Type | image/png |
# "last avatar" also includes the last temporary avatar
And last avatar is a square of size 256
And last avatar is a single "#00FF00" color
Scenario: get user avatar before cropping it
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/green-square-256.png"
# Avatar needs to be cropped to finish setting it even if it is squared
When user "user0" gets avatar for user "user0"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
And last avatar is a square of size 128
And last avatar is not a single color
Scenario: set user avatar from file
Given Logging in using web as "user0"
When logged in user posts temporary avatar from file "data/coloured-pattern.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
Then logged in user gets temporary avatar with 404
And user "user0" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 128
And last avatar is a single "#FF0000" color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 128
And last avatar is a single "#FF0000" color
Scenario: set user avatar from internal path
Given user "user0" uploads file "data/coloured-pattern.png" to "/internal-coloured-pattern.png"
And Logging in using web as "user0"
When logged in user posts temporary avatar from internal path "internal-coloured-pattern.png"
And logged in user crops temporary avatar
| x | 704 |
| y | 320 |
| w | 64 |
| h | 64 |
Then logged in user gets temporary avatar with 404
And user "user0" gets avatar for user "user0" with size "64"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 64
And last avatar is a single "#00FF00" color
And user "anonymous" gets avatar for user "user0" with size "64"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 64
And last avatar is a single "#00FF00" color
Scenario: cropped user avatar needs to be squared
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern.png"
When logged in user crops temporary avatar with 400
| x | 384 |
| y | 256 |
| w | 192 |
| h | 128 |
Scenario: delete user avatar
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
And user "user0" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 128
And last avatar is a single "#FF0000" color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 128
And last avatar is a single "#FF0000" color
When logged in user deletes the user avatar
Then user "user0" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
And last avatar is a square of size 128
And last avatar is not a single color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
And last avatar is a square of size 128
And last avatar is not a single color
Scenario: get user avatar with a larger size than the original one
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
When user "user0" gets avatar for user "user0" with size "192"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 192
And last avatar is a single "#FF0000" color
Scenario: get user avatar with a smaller size than the original one
Given Logging in using web as "user0"
And logged in user posts temporary avatar from file "data/coloured-pattern.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
| w | 128 |
| h | 128 |
When user "user0" gets avatar for user "user0" with size "96"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
And last avatar is a square of size 96
And last avatar is a single "#FF0000" color
Scenario: get default guest avatar
When user "user0" gets avatar for guest "guest0"
Then The following headers should be set
| Content-Type | image/png |
And last avatar is a square of size 128
And last avatar is not a single color
Scenario: get default guest avatar as an anonymous user
When user "anonymous" gets avatar for guest "guest0"
Then The following headers should be set
| Content-Type | image/png |
And last avatar is a square of size 128
And last avatar is not a single color

View file

@ -0,0 +1,271 @@
<?php
/**
* @copyright Copyright (c) 2020, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @author Daniel Calviño Sánchez <danxuliu@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
require __DIR__ . '/../../vendor/autoload.php';
trait Avatar {
/** @var string **/
private $lastAvatar;
/** @AfterScenario **/
public function cleanupLastAvatar() {
$this->lastAvatar = null;
}
private function getLastAvatar() {
$this->lastAvatar = '';
$body = $this->response->getBody();
while (!$body->eof()) {
$this->lastAvatar .= $body->read(8192);
}
$body->close();
}
/**
* @When user :user gets avatar for user :userAvatar
*
* @param string $user
* @param string $userAvatar
*/
public function userGetsAvatarForUser(string $user, string $userAvatar) {
$this->userGetsAvatarForUserWithSize($user, $userAvatar, '128');
}
/**
* @When user :user gets avatar for user :userAvatar with size :size
*
* @param string $user
* @param string $userAvatar
* @param string $size
*/
public function userGetsAvatarForUserWithSize(string $user, string $userAvatar, string $size) {
$this->asAn($user);
$this->sendingToDirectUrl('GET', '/index.php/avatar/' . $userAvatar . '/' . $size);
$this->theHTTPStatusCodeShouldBe('200');
$this->getLastAvatar();
}
/**
* @When user :user gets avatar for guest :guestAvatar
*
* @param string $user
* @param string $guestAvatar
*/
public function userGetsAvatarForGuest(string $user, string $guestAvatar) {
$this->asAn($user);
$this->sendingToDirectUrl('GET', '/index.php/avatar/guest/' . $guestAvatar . '/128');
$this->theHTTPStatusCodeShouldBe('201');
$this->getLastAvatar();
}
/**
* @When logged in user gets temporary avatar
*/
public function loggedInUserGetsTemporaryAvatar() {
$this->loggedInUserGetsTemporaryAvatarWith('200');
}
/**
* @When logged in user gets temporary avatar with :statusCode
*
* @param string $statusCode
*/
public function loggedInUserGetsTemporaryAvatarWith(string $statusCode) {
$this->sendingAToWithRequesttoken('GET', '/index.php/avatar/tmp');
$this->theHTTPStatusCodeShouldBe($statusCode);
$this->getLastAvatar();
}
/**
* @When logged in user posts temporary avatar from file :source
*
* @param string $source
*/
public function loggedInUserPostsTemporaryAvatarFromFile(string $source) {
$file = \GuzzleHttp\Psr7\stream_for(fopen($source, 'r'));
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar',
[
'multipart' => [
[
'name' => 'files[]',
'contents' => $file
]
]
]);
$this->theHTTPStatusCodeShouldBe('200');
}
/**
* @When logged in user posts temporary avatar from internal path :path
*
* @param string $path
*/
public function loggedInUserPostsTemporaryAvatarFromInternalPath(string $path) {
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path);
$this->theHTTPStatusCodeShouldBe('200');
}
/**
* @When logged in user crops temporary avatar
*
* @param TableNode $crop
*/
public function loggedInUserCropsTemporaryAvatar(TableNode $crop) {
$this->loggedInUserCropsTemporaryAvatarWith('200', $crop);
}
/**
* @When logged in user crops temporary avatar with :statusCode
*
* @param string $statusCode
* @param TableNode $crop
*/
public function loggedInUserCropsTemporaryAvatarWith(string $statusCode, TableNode $crop) {
$parameters = [];
foreach ($crop->getRowsHash() as $key => $value) {
$parameters[] = 'crop[' . $key . ']=' . $value;
}
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar/cropped?' . implode('&', $parameters));
$this->theHTTPStatusCodeShouldBe($statusCode);
}
/**
* @When logged in user deletes the user avatar
*/
public function loggedInUserDeletesTheUserAvatar() {
$this->sendingAToWithRequesttoken('DELETE', '/index.php/avatar');
$this->theHTTPStatusCodeShouldBe('200');
}
/**
* @Then last avatar is a square of size :size
*
* @param string size
*/
public function lastAvatarIsASquareOfSize(string $size) {
list($width, $height) = getimagesizefromstring($this->lastAvatar);
Assert::assertEquals($width, $height, 'Avatar is not a square');
Assert::assertEquals($size, $width);
}
/**
* @Then last avatar is not a single color
*/
public function lastAvatarIsNotASingleColor() {
Assert::assertEquals(null, $this->getColorFromLastAvatar());
}
/**
* @Then last avatar is a single :color color
*
* @param string $color
* @param string $size
*/
public function lastAvatarIsASingleColor(string $color) {
$expectedColor = $this->hexStringToRgbColor($color);
$colorFromLastAvatar = $this->getColorFromLastAvatar();
Assert::assertTrue($this->isSameColor($expectedColor, $colorFromLastAvatar),
$this->rgbColorToHexString($colorFromLastAvatar) . ' does not match expected ' . $color);
}
private function hexStringToRgbColor($hexString) {
// Strip initial "#"
$hexString = substr($hexString, 1);
$rgbColorInt = hexdec($hexString);
// RGBA hex strings are not supported; the given string is assumed to be
// an RGB hex string.
return [
'red' => ($rgbColorInt >> 16) & 0xFF,
'green' => ($rgbColorInt >> 8) & 0xFF,
'blue' => $rgbColorInt & 0xFF,
'alpha' => 0
];
}
private function rgbColorToHexString($rgbColor) {
$rgbColorInt = ($rgbColor['red'] << 16) + ($rgbColor['green'] << 8) + ($rgbColor['blue']);
return '#' . str_pad(strtoupper(dechex($rgbColorInt)), 6, '0', STR_PAD_LEFT);
}
private function getColorFromLastAvatar() {
$image = imagecreatefromstring($this->lastAvatar);
$firstPixelColorIndex = imagecolorat($image, 0, 0);
$firstPixelColor = imagecolorsforindex($image, $firstPixelColorIndex);
for ($i = 0; $i < imagesx($image); $i++) {
for ($j = 0; $j < imagesx($image); $j++) {
$currentPixelColorIndex = imagecolorat($image, $i, $j);
$currentPixelColor = imagecolorsforindex($image, $currentPixelColorIndex);
// The colors are compared with a small allowed delta, as even
// on solid color images the resizing can cause some small
// artifacts that slightly modify the color of certain pixels.
if (!$this->isSameColor($firstPixelColor, $currentPixelColor)) {
imagedestroy($image);
return null;
}
}
}
imagedestroy($image);
return $firstPixelColor;
}
private function isSameColor(array $firstColor, array $secondColor, int $allowedDelta = 1) {
if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) &&
$this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) &&
$this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) &&
$this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) {
return true;
}
return false;
}
private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) {
if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) &&
$firstColorComponent <= ($secondColorComponent + $allowedDelta)) {
return true;
}
return false;
}
}

View file

@ -44,6 +44,7 @@ require __DIR__ . '/../../vendor/autoload.php';
trait BasicStructure {
use Auth;
use Avatar;
use Download;
use Mail;
use Trashbin;
@ -178,7 +179,7 @@ trait BasicStructure {
$options = [];
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
} else {
} elseif (strpos($this->currentUser, 'anonymous') !== 0) {
$options['auth'] = [$this->currentUser, $this->regularUser];
}
$options['headers'] = [
@ -218,7 +219,7 @@ trait BasicStructure {
$options = [];
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
} else {
} elseif (strpos($this->currentUser, 'anonymous') !== 0) {
$options['auth'] = [$this->currentUser, $this->regularUser];
}
if ($body instanceof TableNode) {
@ -307,21 +308,31 @@ trait BasicStructure {
* @When Sending a :method to :url with requesttoken
* @param string $method
* @param string $url
* @param TableNode|array|null $body
*/
public function sendingAToWithRequesttoken($method, $url) {
public function sendingAToWithRequesttoken($method, $url, $body = null) {
$baseUrl = substr($this->baseUrl, 0, -5);
$options = [
'cookies' => $this->cookieJar,
'headers' => [
'requesttoken' => $this->requestToken
],
];
if ($body instanceof TableNode) {
$fd = $body->getRowsHash();
$options['form_params'] = $fd;
} elseif ($body) {
$options = array_merge($options, $body);
}
$client = new Client();
try {
$this->response = $client->request(
$method,
$baseUrl . $url,
[
'cookies' => $this->cookieJar,
'headers' => [
'requesttoken' => $this->requestToken
]
]
$options
);
} catch (ClientException $e) {
$this->response = $e->getResponse();