aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@users.noreply.github.com>2020-12-07 09:16:52 +0100
committerGitHub <noreply@github.com>2020-12-07 09:16:52 +0100
commit76ffc46c01eb1efdcb044fe23c4a5bb83c63c184 (patch)
tree201e61ecd08b45156fbb88d5f670e5a401d6bc1b
parenteab710c2fd6febbf946be3ce8376fb386b61ee90 (diff)
parentb4b3276a5be575d6edefca1214c38c2bd0f0b4e5 (diff)
downloadnextcloud-server-76ffc46c01eb1efdcb044fe23c4a5bb83c63c184.tar.gz
nextcloud-server-76ffc46c01eb1efdcb044fe23c4a5bb83c63c184.zip
Merge pull request #24578 from nextcloud/add-integration-tests-for-avatars
-rw-r--r--.drone.yml25
-rw-r--r--build/integration/data/coloured-pattern.pngbin0 -> 2447 bytes
-rw-r--r--build/integration/data/green-square-256.pngbin0 -> 645 bytes
-rw-r--r--build/integration/features/avatar.feature183
-rw-r--r--build/integration/features/bootstrap/Avatar.php271
-rw-r--r--build/integration/features/bootstrap/BasicStructure.php29
6 files changed, 499 insertions, 9 deletions
diff --git a/.drone.yml b/.drone.yml
index aa718998203..e9cacb376fc 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -859,6 +859,31 @@ trigger:
---
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
steps:
diff --git a/build/integration/data/coloured-pattern.png b/build/integration/data/coloured-pattern.png
new file mode 100644
index 00000000000..cf43787f3fd
--- /dev/null
+++ b/build/integration/data/coloured-pattern.png
Binary files differ
diff --git a/build/integration/data/green-square-256.png b/build/integration/data/green-square-256.png
new file mode 100644
index 00000000000..9f14b707ca3
--- /dev/null
+++ b/build/integration/data/green-square-256.png
Binary files differ
diff --git a/build/integration/features/avatar.feature b/build/integration/features/avatar.feature
new file mode 100644
index 00000000000..f7926615c01
--- /dev/null
+++ b/build/integration/features/avatar.feature
@@ -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
diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php
new file mode 100644
index 00000000000..388715340c6
--- /dev/null
+++ b/build/integration/features/bootstrap/Avatar.php
@@ -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;
+ }
+}
diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php
index eed0f173ced..ac5530be5a5 100644
--- a/build/integration/features/bootstrap/BasicStructure.php
+++ b/build/integration/features/bootstrap/BasicStructure.php
@@ -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();