Ctrl K
ComunidadGuías…

tests/rest/RestIssueTest.php

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?php
# MantisBT - A PHP based bugtracking system

# MantisBT is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# MantisBT 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Mantis Webservice Tests
 *
 * @package Tests
 * @subpackage UnitTests
 * @copyright Copyright MantisBT Team - mantisbt-dev@lists.sourceforge.net
 * @link http://www.mantisbt.org
 */

namespace Mantis\tests\rest;

use Psr\Http\Message\ResponseInterface;

/**
 * Test fixture for issue webservice methods.
 *
 * @requires extension curl
 * @group REST
 */
class RestIssueTest extends RestBase {
	/** @var mixed Saved status workflow configuration, or false if unset. */
	private $savedStatusWorkflow = null;

	/** @var bool Whether the status workflow configuration was saved. */
	private bool $statusWorkflowSaved = false;

	/**
	 * @var array $versions
	 */
	protected $versions;

	/**
	 * @var string $tag_name
	 */
	protected $tag_name;

	/**
	 * Mark test Skipped if test project does not have enough versions defined.
	 *
	 * @param $p_num Number of versions
	 * @return void
	 */
	protected function skipTestIfNotEnoughVersions( $p_num ) {
		if( count( $this->versions ) < $p_num ) {
			$this->markTestSkipped(
				"There must be at least $p_num active versions defined in project $this->projectId"
			);
		}
	}


	public function testCreateIssueWithMinimalFields() {
		$t_issue_to_add = $this->getIssueToAdd();
		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode() );
		$t_body = json_decode( $t_response->getBody(), true );
		$t_issue = $t_body['issue'];
		# file_put_contents( '/tmp/response.txt', var_export( $t_body, true ) );

		$this->assertTrue( isset( $t_issue['id'] ), 'id set' );
		$this->assertTrue( is_numeric($t_issue['id'] ), 'id is numeric' );
		$this->assertEquals( $t_issue_to_add['summary'], $t_issue['summary'], 'summary' );
		$this->assertEquals( $t_issue_to_add['description'], $t_issue['description'], 'description' );
		$this->assertEquals( $t_issue_to_add['category']['name'], $t_issue['category']['name'], 'category name' );
		$this->assertEquals( $t_issue_to_add['project']['id'], $t_issue['project']['id'], 'project id' );
		$this->assertEquals( $this->userName, $t_issue['reporter']['name'], 'reporter name' );

		# Verify Status
		$this->assertEquals( 10, $t_issue['status']['id'], 'status id' );
		$this->assertEquals( '#fcbdbd', $t_issue['status']['color'], 'status color' );
		$this->assertEquals( 'new', $t_issue['status']['name'], 'status name' );
		$this->assertEquals( 'new', $t_issue['status']['label'], 'status label' );

		# Verify Resolution
		$this->assertEquals( 10, $t_issue['resolution']['id'], 'resolution id' );
		$this->assertEquals( 'open', $t_issue['resolution']['name'], 'resolution name' );
		$this->assertEquals( 'open', $t_issue['resolution']['label'], 'resolution label' );

		# Verify View State
		$this->assertEquals( 10, $t_issue['view_state']['id'], 'view state id' );
		$this->assertEquals( 'public', $t_issue['view_state']['name'], 'view state name' );
		$this->assertEquals( 'public', $t_issue['view_state']['label'], 'view state label' );

		# Verify Priority
		$this->assertEquals( 30, $t_issue['priority']['id'], 'priority id' );
		$this->assertEquals( 'normal', $t_issue['priority']['name'], 'priority name' );
		$this->assertEquals( 'normal', $t_issue['priority']['label'], 'priority label' );

		# Verify Severity
		$this->assertEquals( 50, $t_issue['severity']['id'], 'severity id' );
		$this->assertEquals( 'minor', $t_issue['severity']['name'], 'severity name' );
		$this->assertEquals( 'minor', $t_issue['severity']['label'], 'severity label' );

		# Verify Reproducibility
		$this->assertEquals( 70, $t_issue['reproducibility']['id'], 'reproducibility id' );
		$this->assertEquals( 'have not tried', $t_issue['reproducibility']['name'], 'reproducibility name' );
		$this->assertEquals( 'have not tried', $t_issue['reproducibility']['label'], 'reproducibility label' );

		$this->assertFalse( $t_issue['sticky'], 'sticky' );
		$this->assertTrue( isset( $t_issue['created_at'] ), 'created at' );
		$this->assertTrue( isset( $t_issue['updated_at'] ), 'updated at' );

		$this->deleteIssueAfterRun( $t_issue['id'] );
	}

	/**
	 * Verify that REST issue updates enforce configured workflow transitions.
	 *
	 * @return void
	 */
	public function testUpdateIssueStatusEnforcesWorkflow() {
		$this->savedStatusWorkflow = $this->setConfig( 'status_enum_workflow', array(
			NEW_ => FEEDBACK . ':feedback',
		) );
		$this->statusWorkflowSaved = true;

		$t_response = $this->builder()->post( '/issues', $this->getIssueToAdd() )->send();
		$t_issue = $this->getJson( $t_response, HTTP_STATUS_CREATED )->issue;
		$this->deleteIssueAfterRun( $t_issue->id );

		$t_response = $this->builder()->patch( '/issues/' . $t_issue->id, array(
			'status' => array( 'id' => ACKNOWLEDGED ),
		) )->send();
		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode() );
		$t_error = json_decode( $t_response->getBody(), true );
		$this->assertStringContainsString( 'Status transition', $t_error['message'] );

		$t_response = $this->builder()->get( '/issues/' . $t_issue->id )->send();
		$t_issue = $this->getJson( $t_response )->issues[0];
		$this->assertEquals( NEW_, $t_issue->status->id );

		$t_response = $this->builder()->patch( '/issues/' . $t_issue->id, array(
			'status' => array( 'id' => FEEDBACK ),
		) )->send();
		$t_issue = $this->getJson( $t_response )->issues[0];
		$this->assertEquals( FEEDBACK, $t_issue->status->id );
	}

	public function testCreateIssueWithLongText() {
		$t_long_text = str_repeat( 'x', config_get_global( 'max_textarea_length' ) );

		$t_fields = [ 'description', 'steps_to_reproduce', 'additional_information' ];
		foreach( $t_fields as $t_field ) {
			$t_issue_to_add = $this->getIssueToAdd( $t_field );

			# Maximum length
			$t_issue_to_add[$t_field] = $t_long_text;
			$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
			$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode(),
				"Creating an issue with '$t_field' at maximum size should succeed"
			);
			$t_body = json_decode( $t_response->getBody(), true );
			$t_issue = $t_body['issue'];
			$this->deleteIssueAfterRun( $t_issue['id'] );

			# Too long
			$t_issue_to_add[$t_field] .= 'TOO LONG';
			$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
			$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode(),
				"Creating an issue with '$t_field' longer than max size should fail"
			);
		}

		sleep(20);
	}

	/**
	 * Tests that summary length validation counts characters, not bytes.
	 * A 128-character multibyte string should succeed even though it
	 * exceeds 128 bytes, while 129 characters should fail.
	 *
	 * @return void
	 */
	public function testCreateIssueWithTooLongSummary() {
		# 128 CJK characters = 384 bytes in UTF-8, but only 128 characters
		$t_issue_to_add = $this->getIssueToAdd( 'summary-multibyte' );
		$t_issue_to_add['summary'] = str_repeat( "\xE4\xB8\x96", DB_FIELD_SIZE_BUG_SUMMARY );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode(),
			'Creating an issue with exactly ' . DB_FIELD_SIZE_BUG_SUMMARY . ' multibyte characters should succeed'
		);

		$t_body = json_decode( $t_response->getBody(), true );
		$this->deleteIssueAfterRun( $t_body['issue']['id'] );

		# 129 CJK characters should fail
		$t_issue_to_add['summary'] = str_repeat( "\xE4\xB8\x96", DB_FIELD_SIZE_BUG_SUMMARY + 1 );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode(),
			'Creating an issue with ' . ( DB_FIELD_SIZE_BUG_SUMMARY + 1 ) . ' multibyte characters should fail'
		);
	}

	/**
	 * Summary is a single-line field and must reject line breaks.
	 *
	 * @return void
	 */
	public function testSummaryCannotContainLineBreaks() {
		$t_issue_to_add = $this->getIssueToAdd( 'summary-newline' );
		$t_issue_to_add['summary'] .= "\nsecond line";

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode(),
			'Creating an issue with a multiline summary should fail'
		);
	}

	/**
	 * Summary is a single-line field and must reject line breaks on update.
	 *
	 * @return void
	 */
	public function testUpdateIssueWithSummaryContainingLineBreaks() {
		$t_issue_to_add = $this->getIssueToAdd( 'summary-update-newline' );
		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode() );
		$t_issue_id = json_decode( $t_response->getBody(), true )['issue']['id'];
		$this->deleteIssueAfterRun( $t_issue_id );

		$t_response = $this->builder()->patch(
			'/issues/' . $t_issue_id,
			array( 'summary' => $t_issue_to_add['summary'] . "\rsecond line" )
		)->send();
		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode(),
			'Updating an issue with a multiline summary should fail'
		);
	}

	public function testCreateIssueWithEnumIds() {
		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['status']['id'] = 50; # assigned
		$t_issue_to_add['resolution']['id'] = 30; # reopened
		$t_issue_to_add['view_state']['id'] = 50; # private
		$t_issue_to_add['priority']['id'] = 40; # high
		$t_issue_to_add['severity']['id'] = 10; # feature
		$t_issue_to_add['reproducibility']['id'] = 10; # always
		$t_issue_to_add['sticky'] = true;

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode() );
		$t_body = json_decode( $t_response->getBody(), true );
		$t_issue = $t_body['issue'];

		$this->assertTrue( isset( $t_issue['id'] ) );
		$this->assertTrue( is_numeric($t_issue['id'] ) );

		# Verify Status
		$this->assertEquals( 50, $t_issue['status']['id'] );
		$this->assertEquals( '#c2dfff', $t_issue['status']['color'] );
		$this->assertEquals( 'assigned', $t_issue['status']['name'] );
		$this->assertEquals( 'assigned', $t_issue['status']['label'] );

		# Verify Resolution
		$this->assertEquals( 30, $t_issue['resolution']['id'], 'resolution id' );
		$this->assertEquals( 'reopened', $t_issue['resolution']['name'], 'resolution name' );
		$this->assertEquals( 'reopened', $t_issue['resolution']['label'], 'resolution label' );

		# Verify View State
		$this->assertEquals( 50, $t_issue['view_state']['id'], 'view state id' );
		$this->assertEquals( 'private', $t_issue['view_state']['name'], 'view state name' );
		$this->assertEquals( 'private', $t_issue['view_state']['label'], 'view state label' );

		# Verify Priority
		$this->assertEquals( 40, $t_issue['priority']['id'], 'priority id' );
		$this->assertEquals( 'high', $t_issue['priority']['name'], 'priority name' );
		$this->assertEquals( 'high', $t_issue['priority']['label'], 'priority label' );

		# Verify Severity
		$this->assertEquals( 10, $t_issue['severity']['id'], 'severity id' );
		$this->assertEquals( 'feature', $t_issue['severity']['name'], 'severity name' );
		$this->assertEquals( 'feature', $t_issue['severity']['label'], 'severity label' );

		# Verify Reproducibility
		$this->assertEquals( 10, $t_issue['reproducibility']['id'], 'reproducibility id' );
		$this->assertEquals( 'always', $t_issue['reproducibility']['name'], 'reproducibility name' );
		$this->assertEquals( 'always', $t_issue['reproducibility']['label'], 'reproducibility label' );

		$this->assertTrue( $t_issue['sticky'], 'sticky' );
		$this->assertTrue( isset( $t_issue['created_at'] ), 'created at' );
		$this->assertTrue( isset( $t_issue['updated_at'] ), 'updated at' );

		$this->deleteIssueAfterRun( $t_issue['id'] );
	}

	public function testCreateIssueWithVersionString() {
		$this->skipTestIfNotEnoughVersions( 3 );

		$t_version_name = $this->versions[2]['version'];
		$t_target_version_name = $this->versions[1]['version'];
		$t_fixed_in_version_name = $this->versions[0]['version'];

		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['version'] = $t_version_name;
		$t_issue_to_add['target_version'] = $t_target_version_name;
		$t_issue_to_add['fixed_in_version'] = $t_fixed_in_version_name;

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode() );
		$t_body = json_decode( $t_response->getBody(), true );
		$t_issue = $t_body['issue'];

		$this->assertTrue( isset( $t_issue['version'] ), 'version id set' );
		$this->assertTrue( isset( $t_issue['version']['id'] ), 'version id set' );
		$this->assertTrue( isset( $t_issue['version']['name'] ), 'version name set' );
		$this->assertEquals( $t_version_name, $t_issue['version']['name'] );
		$this->assertEquals( $t_target_version_name, $t_issue['target_version']['name'] );
		$this->assertEquals( $t_fixed_in_version_name, $t_issue['fixed_in_version']['name'] );

		$this->deleteIssueAfterRun( $t_issue['id'] );
	}

	public function testCreateIssueWithVersionObjectName() {
		$this->skipTestIfNotEnoughVersions( 1 );

		$t_version_name = $this->versions[0]['version'];

		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['version'] = array( 'name' => $t_version_name );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode() );
		$t_body = json_decode( $t_response->getBody(), true );
		$t_issue = $t_body['issue'];

		$this->assertTrue( isset( $t_issue['version'] ), 'version id set' );
		$this->assertTrue( isset( $t_issue['version']['id'] ), 'version id set' );
		$this->assertTrue( is_numeric( $t_issue['version']['id'] ), 'version id numeric' );
		$this->assertTrue( isset( $t_issue['version']['name'] ), 'version name set' );
		$this->assertEquals( $t_version_name, $t_issue['version']['name'] );
		$this->assertFalse( isset( $t_issue['target_version'] ), 'target_version set' );
		$this->assertFalse( isset( $t_issue['fixed_in_version'] ), 'fixed_in_version set' );

		$this->deleteIssueAfterRun( $t_issue['id'] );
	}

	public function testCreateIssueWithVersionObjectId() {
		$this->skipTestIfNotEnoughVersions( 1 );

		$t_version_name = $this->versions[0]['version'];
		$t_version_id = $this->versions[0]['id'];

		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['version'] = array( 'id' => $t_version_id );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode() );
		$t_body = json_decode( $t_response->getBody(), true );
		$t_issue = $t_body['issue'];

		$this->assertTrue( isset( $t_issue['version'] ), 'version set' );
		$this->assertTrue( isset( $t_issue['version']['id'] ), 'version id set' );
		$this->assertTrue( is_numeric( $t_issue['version']['id'] ), 'version id numeric' );
		$this->assertTrue( isset( $t_issue['version']['name'] ), 'version name set' );
		$this->assertEquals( $t_version_name, $t_issue['version']['name'], 'version name' );
		$this->assertEquals( $t_version_id, $t_issue['version']['id'], 'version id' );
		$this->assertFalse( isset( $t_issue['target_version'] ), 'target_version set' );
		$this->assertFalse( isset( $t_issue['fixed_in_version'] ), 'fixed_in_version set' );

		$this->deleteIssueAfterRun( $t_issue['id'] );
	}

	public function testCreateIssueWithVersionObjectIdAndMismatchingName() {
		$this->skipTestIfNotEnoughVersions( 2 );

		$t_version_id = $this->versions[0]['id'];
		$t_wrong_version_name = $this->versions[1]['version'];
		$t_correct_version_name = $this->versions[0]['version'];

		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['version'] = array( 'id' => $t_version_id, 'name' => $t_wrong_version_name );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode() );
		$t_body = json_decode( $t_response->getBody(), true );
		$t_issue = $t_body['issue'];

		$this->assertTrue( isset( $t_issue['version'] ), 'version id set' );
		$this->assertTrue( isset( $t_issue['version']['id'] ), 'version id set' );
		$this->assertTrue( is_numeric( $t_issue['version']['id'] ), 'version id numeric' );
		$this->assertTrue( isset( $t_issue['version']['name'] ), 'version name set' );
		$this->assertEquals( $t_correct_version_name, $t_issue['version']['name'], 'version name' );
		$this->assertEquals( $t_version_id, $t_issue['version']['id'], 'version id' );
		$this->assertFalse( isset( $t_issue['target_version'] ), 'target_version set' );
		$this->assertFalse( isset( $t_issue['fixed_in_version'] ), 'fixed_in_version set' );

		$this->deleteIssueAfterRun( $t_issue['id'] );
	}

	public function testCreateIssueWithVersionObjectIdNotFound() {
		# Test case assumes webservice_error_when_version_not_found = ON.
		$t_version_id = 10000;
		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['version'] = array( 'id' => $t_version_id );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode() );
	}

	public function testCreateIssueWithVersionObjectNameNotFound() {
		# Test case assumes webservice_error_when_version_not_found = ON.
		$t_version_name = 'VersionNotFound';
		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['version'] = array( 'name' => $t_version_name );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode() );
	}

	public function testCreateIssueWithVersionStringNotFound() {
		# Test case assumes webservice_error_when_version_not_found = ON.
		$t_version_name = 'VersionNotFound';
		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['version'] = $t_version_name;

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode() );
	}

	/**
	 * New tag should be created and attached to a new issue
	 */
	public function testCreateIssueWithTagNotExisting() {
		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['tags'] = array( array( 'name' => $this->tag_name ) );

		# Change threshold to disable tag creation
		$t_threshold = config_set( 'tag_create_threshold', NOBODY );
		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$this->assertEquals(
			HTTP_STATUS_NOT_FOUND,
			$t_response->getStatusCode(),
			'New issue with non-existing tag while not allowed to create tags'
		);

		# Reset threshold and try again
		config_set( 'tag_create_threshold', $t_threshold );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$t_issue_id = $this->assertIssueCreatedWithTag( $t_response );

		$this->deleteIssueAfterRun( $t_issue_id );
	}

	public function testCreateIssueWithTagExisting() {
		$t_issue_to_add = $this->getIssueToAdd();

		# Tag by name
		$t_issue_to_add['tags'] = array( array( 'name' => $this->tag_name ) );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$t_issue_id = $this->assertIssueCreatedWithTag( $t_response );

		$this->deleteIssueAfterRun( $t_issue_id );

		# Tag by id
		# TODO: replace internal call by GET /tag request when implemented (see #32863)
		$t_tag = tag_get_by_name( $this->tag_name );
		$t_issue_to_add['tags'] = array( array( 'id' => $t_tag['id'] ) );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$t_issue_id = $this->assertIssueCreatedWithTag( $t_response );

		$this->deleteIssueAfterRun( $t_issue_id );
	}

	/**
	 * Checks that the issue was created successfully and the tag was properly attached.
	 *
	 * @param ResponseInterface $p_response
	 *
	 * @return integer Created issue ID
	 */
	protected function assertIssueCreatedWithTag( $p_response ) {
		$this->assertEquals( HTTP_STATUS_CREATED, $p_response->getStatusCode() );

		$t_body = json_decode( $p_response->getBody(), true );
		$t_issue = $t_body['issue'];

		$this->assertTrue( isset( $t_issue['tags'] ), 'tags set' );
		$this->assertEquals( $this->tag_name, $t_issue['tags'][0]['name'] );

		return $t_issue['id'];
	}

	/**
	 * Tests creation of issues with invalid tags.
	 *
	 * @param mixed   $p_tag         Tag element
	 * @param integer $p_status_code Expected status code
	 *
	 * @dataProvider providerTagsInvalid
	 */
	public function testCreateIssueWithTagInvalid( $p_tag, $p_status_code ) {
		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['tags'] = array( $p_tag );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$this->assertEquals( $p_status_code, $t_response->getStatusCode() );
	}

	/**
	 * Provide a series of invalid Tag elements.
	 *
	 * Test case structure:
	 *   <case> => array( <tag element>, <error code> )
	 *
	 * Creating an issue with <tag element> should fail to with <error code>.
	 *
	 * @return array List of test cases
	 *
	 */
	public static function providerTagsInvalid() {
		return array(
			'EmptyTagElement' => array(
				array(),
				HTTP_STATUS_BAD_REQUEST
			),

			'NotATagElement' => array(
				array( 'what' => 'ever' ),
				HTTP_STATUS_BAD_REQUEST
			),

			'InvalidTagId' => array(
				array( 'id' => -1 ),
				HTTP_STATUS_NOT_FOUND
			),

			'EmptyTagName' => array(
				array( 'name' => '' ),
				HTTP_STATUS_BAD_REQUEST
			),
		);
	}

	public function testTagAttachDetach() {
		# Create test issue
		$t_issue_to_add = $this->getIssueToAdd();
		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();
		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode() );
		$t_body = json_decode( $t_response->getBody(), true );
		$t_issue_id = $t_body['issue']['id'];
		$this->deleteIssueAfterRun( $t_issue_id );

		$t_url_base = "/issues/$t_issue_id/tags/";

		# TODO: replace internal call by GET /tag request when implemented (see #32863)
		$this->assertFalse( tag_get_by_name( $this->tag_name ), "The Tag already exists" );

		# Attach the tag - it will be created
		$t_data = $this->getTagData();
		$t_response = $this->builder()->post( $t_url_base, $t_data )->send();
		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode(),
			"Failed to attach the tag"
		);

		# TODO: replace internal call by GET /tag request when implemented (see #32863)
		$t_tag_id = tag_get_by_name( $this->tag_name )['id'];
		$this->assertNotFalse( $t_tag_id, "Tag has not been created" );

		$t_body = json_decode( $t_response->getBody() );
		$t_issue_tags = array_column( $t_body->issues[0]->tags ?? [], 'id' );
		$this->assertContains( $t_tag_id, $t_issue_tags,
			"Tag does not exist in created Issue data"
		);

		# Attach the same tag again
		$t_response = $this->builder()->post( $t_url_base, $t_data )->send();
		$this->assertEquals( HTTP_STATUS_CREATED, $t_response->getStatusCode(),
			"Failed to attach the same tag"
		);

		# Detach the tag
		$t_response = $this->builder()->delete( $t_url_base . $t_tag_id )->send();
		$this->assertEquals( HTTP_STATUS_SUCCESS, $t_response->getStatusCode(),
			"Tag was not detached"
		);

		# Try to detach the same tag again
		$t_response = $this->builder()->delete( $t_url_base . $t_tag_id )->send();
		$this->assertEquals( HTTP_STATUS_SUCCESS, $t_response->getStatusCode(),
			"Attempting to detach an unattached tag should have succeeded"
		);

		# Try to detach a non-existing tag
		$t_tag_id = 99999;
		while( tag_exists( $t_tag_id ) ) {
			$t_tag_id++;
		}
		$t_response = $this->builder()->delete( "/issues/$t_issue_id/tags/$t_tag_id" )->send();
		$this->assertEquals( HTTP_STATUS_NOT_FOUND, $t_response->getStatusCode(),
			"Detaching a non-existing tag should have failed"
		);
	}

	public function testTagAttachDetachNonExistingIssue() {
		$t_nonexistent_issue_id = 99999;
		while( bug_exists( $t_nonexistent_issue_id ) ) {
			$t_nonexistent_issue_id++;
		}
		$t_url_base = "/issues/$t_nonexistent_issue_id/tags/";

		$t_response = $this->builder()->post( $t_url_base, $this->getTagData() )->send();
		$this->assertEquals( HTTP_STATUS_NOT_FOUND, $t_response->getStatusCode(),
			"Attaching a tag to a non-existing issue should have failed"
		);

		# TODO: replace internal calls by GET /tag request when implemented (see #32863)
		$t_tag_id = tag_create( $this->tag_name );
		$this->assertTrue( tag_exists( $t_tag_id ) );
		$t_response = $this->builder()->delete( $t_url_base . $t_tag_id )->send();
		$this->assertEquals( HTTP_STATUS_NOT_FOUND, $t_response->getStatusCode(),
			"Detaching an existing tag from a non-existing issue should have failed"
		);
	}

	public function testCreateIssueNoSummary() {
		$t_issue_to_add = $this->getIssueToAdd();
		unset( $t_issue_to_add['summary'] );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode() );
	}

	public function testCreateIssueNoDescription() {
		$t_issue_to_add = $this->getIssueToAdd();
		unset( $t_issue_to_add['description'] );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode() );
	}

	public function testCreateIssueNoCategory() {
		$t_allow_no_category = config_get( 'allow_no_category' );
		$t_result = $t_allow_no_category ? HTTP_STATUS_CREATED : HTTP_STATUS_BAD_REQUEST;

		$t_issue_to_add = $this->getIssueToAdd();
		unset( $t_issue_to_add['category'] );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( $t_result, $t_response->getStatusCode() );

		if( $t_response->getStatusCode() == HTTP_STATUS_CREATED ) {
			$t_body = json_decode( $t_response->getBody(), true );
			$t_issue = $t_body['issue'];
			$this->deleteIssueAfterRun( $t_issue['id'] );
		}
	}

	public function testCreateIssueWithCategoryInvalid() {
		$t_issue_to_add = $this->getIssueToAdd();
		$t_issue_to_add['category'] = 'Non-existent category';

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_NOT_FOUND, $t_response->getStatusCode() );

		if( $t_response->getStatusCode() == HTTP_STATUS_CREATED ) {
			$t_body = json_decode( $t_response->getBody(), true );
			$t_issue = $t_body['issue'];
			$this->deleteIssueAfterRun( $t_issue['id'] );
		}
	}

	public function testCreateIssueNoProject() {
		$t_issue_to_add = $this->getIssueToAdd();
		unset( $t_issue_to_add['project'] );

		$t_response = $this->builder()->post( '/issues', $t_issue_to_add )->send();

		$this->assertEquals( HTTP_STATUS_BAD_REQUEST, $t_response->getStatusCode() );
	}

	public function setUp(): void {
		parent::setUp();

		# Retrieve the 3 most recent versions
		$this->dbConnect();
		$t_versions = version_get_all_rows( $this->projectId, null, true );
		$this->versions = array_slice( $t_versions, 0, 3 );

		# Generate a unique tag name
		do {
			$this->tag_name = 'new-tag-' . rand();
		} while( !tag_is_unique( $this->tag_name ) );
	}

	public function tearDown(): void {
		parent::tearDown();

		if( $this->statusWorkflowSaved ) {
			$this->restoreConfig( 'status_enum_workflow', $this->savedStatusWorkflow );
		}

		# Delete tag if it exists
		# TODO: replace internal calls by GET /tag request when implemented (see #32863)
		$t_tag = tag_get_by_name( $this->tag_name );
		if( $t_tag ) {
			# Must be logged in to delete tag
			/** @noinspection PhpUnhandledExceptionInspection */
			auth_attempt_script_login( $this->userName, $this->password );

			tag_delete( $t_tag['id'] );
		}
	}

	/**
	 * Generates Tag Data payload for Tag Attach endpoint.
	 *
	 * @return array[]
	 */
	private function getTagData(): array {
		return [
			'tags' => [
				[ 'name' => $this->tag_name ],
			]
		];
	}
}