mirror of
https://github.com/nextcloud/server.git
synced 2026-05-25 02:34:12 -04:00
Merge pull request #24578 from nextcloud/add-integration-tests-for-avatars
This commit is contained in:
commit
76ffc46c01
6 changed files with 499 additions and 9 deletions
25
.drone.yml
25
.drone.yml
|
|
@ -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
|
||||
|
|
|
|||
BIN
build/integration/data/coloured-pattern.png
Normal file
BIN
build/integration/data/coloured-pattern.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
build/integration/data/green-square-256.png
Normal file
BIN
build/integration/data/green-square-256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 645 B |
183
build/integration/features/avatar.feature
Normal file
183
build/integration/features/avatar.feature
Normal 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
|
||||
271
build/integration/features/bootstrap/Avatar.php
Normal file
271
build/integration/features/bootstrap/Avatar.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue