From 2cc22a06b4d56e1d46d1de45993b3149455c3bb4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= <danxuliu@gmail.com>
Date: Thu, 3 Dec 2020 14:19:43 +0100
Subject: [PATCH] Add integration tests for user avatars
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
---
 .drone.yml                                    |  25 +++
 build/integration/data/coloured-pattern.png   | Bin 0 -> 2447 bytes
 build/integration/data/green-square-256.png   | Bin 0 -> 645 bytes
 build/integration/features/avatar.feature     | 135 ++++++++++++
 .../integration/features/bootstrap/Avatar.php | 208 ++++++++++++++++++
 .../features/bootstrap/BasicStructure.php     |   1 +
 6 files changed, 369 insertions(+)
 create mode 100644 build/integration/data/coloured-pattern.png
 create mode 100644 build/integration/data/green-square-256.png
 create mode 100644 build/integration/features/avatar.feature
 create mode 100644 build/integration/features/bootstrap/Avatar.php

diff --git a/.drone.yml b/.drone.yml
index aa718998203..e9cacb376fc 100644
--- a/.drone.yml
+++ b/.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
diff --git a/build/integration/data/coloured-pattern.png b/build/integration/data/coloured-pattern.png
new file mode 100644
index 0000000000000000000000000000000000000000..cf43787f3fda7b081bb113e6843fa332c4a5108a
GIT binary patch
literal 2447
zcmeAS@N?(olHy`uVBq!ia0y~yU;#3j7&w@K)a3P@?|>9%fk$L90|U1(2s1Lwnj--e
zWH0gbb!C6T!^9&ab9IW|380W{W=KSdbAE1aYF-JD%fR4Vl$uzQnxasiS(2gP?&%wl
zqL<1JG>_HO#WAGf*4vwmyayBnSPs@TuS<Hnc45z}A9s3MxVPL4;AhDeo>22ImLZ0r
z<I60dNgzil05Jn25OV;r0}un9$-p20#0@}<ssy(TcFjZ@3)75gAxs9ln;bw69O7<L
z0QrH0aKauj)D0&THziEFn#<C(lYznRzrZnWpkwjdh3S_ep2)x+8uE$E2N-em^h87A
zt2xQ6Nww9|jd$b^Y-QAAQ27M1pPp{OOo|wp#{uXhQrx5fQcpr|bpR@F0Akc)6n_qK
z0I4S-oD@LnNpKTJAP~^Z!2qnG8bFqikdqxi>dEy)!i-(lIb7~DNI&@DtZ;mON*vHE
ziVP$|GwGp1nwv-nJ9vByHaC$FpAEn$V_-}G>Y%G9cs?`#<`S-nXuG%z<N!}sKbLh*
G2~7ZJca3xa

literal 0
HcmV?d00001

diff --git a/build/integration/data/green-square-256.png b/build/integration/data/green-square-256.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f14b707ca369534f80cc2be75f5dfcd97642fc3
GIT binary patch
literal 645
zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911L)MWvCLm<Uj;1OBOz`!jG!i)^F=12eq
z*-JcqUD==TF!OSV_{^{Q3lx&g42dXl&d<$F%_{+N85o?4QWHy3QxwWGOEMJPJ$(aG
z^itV@rvLVIaSW-r_4cwOF9QR|kqylM=RNFYi{kVSn|UBwmywNuxq<NjLjr>Vg9HN)
w1GTxdF%`+ZNKBgegxUdu;a*yVI>eF~?g#~@|DmDUoS^jK>FVdQ&MBb@061uAZ~y=R

literal 0
HcmV?d00001

diff --git a/build/integration/features/avatar.feature b/build/integration/features/avatar.feature
new file mode 100644
index 00000000000..8580471ef55
--- /dev/null
+++ b/build/integration/features/avatar.feature
@@ -0,0 +1,135 @@
+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
diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php
new file mode 100644
index 00000000000..215a3386ab8
--- /dev/null
+++ b/build/integration/features/bootstrap/Avatar.php
@@ -0,0 +1,208 @@
+<?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 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) {
+		Assert::assertEquals($color, $this->getColorFromLastAvatar());
+	}
+
+	private function getColorFromLastAvatar() {
+		$image = imagecreatefromstring($this->lastAvatar);
+
+		$firstPixelColor = imagecolorat($image, 0, 0);
+
+		for ($i = 0; $i < imagesx($image); $i++) {
+			for ($j = 0; $j < imagesx($image); $j++) {
+				$currentPixelColor = imagecolorat($image, $i, $j);
+
+				if ($firstPixelColor !== $currentPixelColor) {
+					imagedestroy($image);
+
+					return null;
+				}
+			}
+		}
+
+		imagedestroy($image);
+
+		// Assume that the image is a truecolor image and thus the index is the
+		// RGB value of the pixel as an integer.
+		return '#' . str_pad(strtoupper(dechex($firstPixelColor)), 6, '0', STR_PAD_LEFT);
+	}
+}
diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php
index 4775a23b902..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;
-- 
GitLab