aboutsummaryrefslogtreecommitdiffstats
path: root/tests/unit/Module/AdminAccountEditTest.php
blob: fe682c527d9c4ec86c5d4105946bf2990105cae5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
<?php
/* Tests for Account_edit module
 *
 * SPDX-FileCopyrightText: 2024 Hubzilla Community
 * SPDX-FileContributor: Harald Eilertsen
 *
 * SPDX-License-Identifier: MIT
 */

namespace Zotlabs\Tests\Unit\Module;

use DateTimeImmutable;
use PHPUnit\Framework\Attributes\{Before, After};
use Zotlabs\Model\Account;

class AdminAccountEditTest extends TestCase {

	private $stub_is_site_admin;
	private $stub_info;
	private $stub_notice;
	private $stub_check_security;
	private $stub_get_form_security_token;

	private array $info;
	private array $notice;

	#[Before]
	public function setup_mocks(): void {
		/*
		 * As we're testing pages that should only be reachable by the
		 * site admin, it makes no sense to have it return anything else
		 * than true.
		 */
		$this->stub_is_site_admin =
			$this->getFunctionMock('Zotlabs\Module', 'is_site_admin')
				->expects($this->once())
				->willReturn(true);

		$this->info = [];
		$this->stub_info =
			$this->getFunctionMock('Zotlabs\Module\Admin', 'info')
				->expects($this->any())
				->willReturnCallback(function (string $arg) {
					$this->info[] = $arg;
				});

		$this->notice = [];
		$this->stub_notice =
			$this->getFunctionMock('Zotlabs\Module\Admin', 'notice')
				->expects($this->any())
				->willReturnCallback(function (string $arg) {
					$this->notice[] = $arg;
				});

	}

	#[After]
	public function tear_down_mocks(): void {
		$this->stub_is_site_admin = null;
		$this->stub_info = null;
		$this->stub_notice = null;
		$this->stub_check_security = null;
		$this->stub_get_form_security_token = null;
	}

	public function test_rendering_admin_account_edit_page(): void {
		$this->stub_get_form_security_token =
			$this->getFunctionMock('Zotlabs\Module\Admin', 'get_form_security_token')
				->expects($this->once())
				->willReturn('the-csrf-token');

		$account = $this->fixtures['account'][0];

		$this->get("admin/account_edit/{$account['account_id']}");

		$this->assertPageContains("<form action=\"admin/account_edit/{$account['account_id']}\" method=\"post\"");
		$this->assertPageContains($account['account_email']);

		// Check that we generate a CSRF token for the form
		$this->assertPageContains("<input type=\"hidden\" name=\"security\" value=\"the-csrf-token\"");
	}

	public function test_rendering_admin_account_edit_page_fails_if_id_is_not_found(): void {
		$this->get("admin/account_edit/666");

		$this->assertEquals('', \App::$page['content']);
	}

	public function test_rendering_admin_account_edit_page_fails_if_id_is_not_numeric(): void {
		$this->get("admin/account_edit/66invalid");

		$this->assertEquals('', \App::$page['content']);
	}

	public function test_post_empty_form_does_not_modify_account(): void {
		$this->stub_goaway();
		$this->stub_check_form_security(true);

		$account = get_account_by_id($this->fixtures['account'][0]['account_id']);

		try {
			$this->post(
				"admin/account_edit/{$account['account_id']}",
				[],
				[
					'aid' => $account['account_id'],
					'pass1' => '',
					'pass2' => '',
					'service_class' => $account['account_service_class'],
					'account_language' => $account['account_language'],
					'security' => 'The security token',
				]
			);
		} catch (RedirectException $ex) {
			$this->assertEquals(z_root() . '/admin/accounts', $ex->getMessage());
		}

		$reloaded = get_account_by_id($account['account_id']);

		$this->assertEquals($account, $reloaded);

		// Not sure if this is expected behaviour, but this is how it is today.
		$this->assertContains('Account settings updated.' . EOL, $this->info);
	}

	public function test_post_form_changes_account(): void {
		$this->stub_goaway();
		$this->stub_check_form_security(true);

		// clone account from fixture, to ensure it's not replaced with
		// the reloaded one below.
		$account = get_account_by_id($this->fixtures['account'][0]['account_id']);

		try {
			$this->post(
				"admin/account_edit/{$account['account_id']}",
				[],
				[
					'aid' => $account['account_id'],
					'pass1' => 'hunter2',
					'pass2' => 'hunter2',
					'service_class' => 'Some other class',
					'account_language' => 'nn',
					'security' => 'The security token',
				]
			);
		} catch (RedirectException $ex) {
			$this->assertEquals(z_root() . '/admin/accounts', $ex->getMessage());
		}

		$reloaded = get_account_by_id($account['account_id']);

		$this->assertNotEquals($account, $reloaded);
		$this->assertEquals('Some other class', $reloaded['account_service_class']);
		$this->assertEquals('nn', $reloaded['account_language']);

		$now = new DateTimeImmutable('now');
		$this->assertEquals($now->format('Y-m-d H:i:s'), $reloaded['account_password_changed']);

		$this->assertContains('Account settings updated.' . EOL, $this->info);
		$this->assertContains("Password changed for account {$account['account_id']}." . EOL, $this->info);
	}

	public function test_form_with_missing_or_incalid_csrf_token_is_rejected(): void {
		$this->expectException(KillmeException::class);

		// Emulate a failed CSRF check
		$this->stub_check_form_security(false);

		$account_id = $this->fixtures['account'][0]['account_id'];

		$this->post(
			"admin/account_edit/{$account_id}",
			[],
			[
				'aid' => $account_id,
				'pass1' => 'hunter2',
				'pass2' => 'hunter2',
				'service_class' => 'Some other class',
				'account_language' => 'nn',
				'security' => 'Invalid security token',
			]
		);
	}

	/*
	 * Override the stub_goaway method because we need the stub to live in the
	 * Admin namespace.
	 */
	protected function stub_goaway(): void {
		$this->goaway_stub = $this->getFunctionMock('Zotlabs\Module\Admin', 'goaway')
			->expects($this->once())
			->willReturnCallback(
				function (string $uri) {
					throw new RedirectException($uri);
				}
			);
	}

	/**
	 * Stub the check_form_security_token_ForbiddenOnErr.
	 *
	 * In these tests we're not really interested in _how_ the form security
	 * tokens work, but that the code under test perform the checks. This stub
	 * allows us to do that without having to worry if everything is set up so
	 * that the real function would work or not.
	 *
	 * @param bool $valid		true if emulating a valid token, false otherwise.
	 */
	protected function stub_check_form_security(bool $valid): void {
		$this->stub_check_security =
			$this->getFunctionMock('Zotlabs\Module\Admin', 'check_form_security_token_ForbiddenOnErr')
				->expects($this->once())
				->with(
					$this->identicalTo('admin_account_edit'),
					$this->identicalTo('security'))
				->willReturnCallback(function () use ($valid) {
					if (! $valid) {
						throw new KillmeException();
					}
				});
	}
}