2020-01-11 13:58:02 +00:00
< ? php
2020-01-12 06:35:37 +00:00
namespace Tests\E2E\Services\Account ;
2020-01-11 13:58:02 +00:00
2026-01-15 03:14:53 +00:00
use PHPUnit\Framework\Attributes\Group ;
2020-01-11 13:58:02 +00:00
use Tests\E2E\Client ;
2023-02-05 20:07:46 +00:00
use Utopia\Database\Helpers\ID ;
2023-03-01 12:00:36 +00:00
use Utopia\Database\Validator\Datetime as DatetimeValidator ;
2026-01-13 09:01:22 +00:00
use Utopia\System\System ;
2020-01-11 13:58:02 +00:00
trait AccountBase
{
2022-06-02 12:47:07 +00:00
public function testCreateAccount () : array
2020-01-11 13:58:02 +00:00
{
2022-06-02 12:47:07 +00:00
$email = uniqid () . 'user@localhost.test' ;
2020-01-11 21:53:57 +00:00
$password = 'password' ;
2020-01-11 13:58:02 +00:00
$name = 'User Name' ;
/**
* Test for SUCCESS
*/
2020-01-12 21:28:26 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
2020-01-11 13:58:02 +00:00
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
2020-02-17 07:16:11 +00:00
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2020-01-12 21:28:26 +00:00
]), [
2022-08-14 10:33:36 +00:00
'userId' => ID :: unique (),
2020-01-11 13:58:02 +00:00
'email' => $email ,
'password' => $password ,
'name' => $name ,
]);
2020-02-17 07:16:11 +00:00
$id = $response [ 'body' ][ '$id' ];
2025-11-10 13:00:18 +00:00
2023-10-26 14:04:47 +00:00
$this -> assertEquals ( 201 , $response [ 'headers' ][ 'status-code' ]);
2020-01-11 13:58:02 +00:00
$this -> assertNotEmpty ( $response [ 'body' ]);
2020-02-17 07:16:11 +00:00
$this -> assertNotEmpty ( $response [ 'body' ][ '$id' ]);
2023-02-05 20:39:41 +00:00
$this -> assertEquals ( true , ( new DatetimeValidator ()) -> isValid ( $response [ 'body' ][ 'registration' ]));
2020-01-11 13:58:02 +00:00
$this -> assertEquals ( $response [ 'body' ][ 'email' ], $email );
$this -> assertEquals ( $response [ 'body' ][ 'name' ], $name );
2023-05-27 00:15:38 +00:00
$this -> assertEquals ( $response [ 'body' ][ 'labels' ], []);
2023-07-07 00:12:39 +00:00
$this -> assertArrayHasKey ( 'accessedAt' , $response [ 'body' ]);
$this -> assertNotEmpty ( $response [ 'body' ][ 'accessedAt' ]);
2025-07-29 06:27:57 +00:00
$this -> assertArrayHasKey ( 'targets' , $response [ 'body' ]);
$this -> assertEquals ( $email , $response [ 'body' ][ 'targets' ][ 0 ][ 'identifier' ]);
2025-11-10 13:00:18 +00:00
$this -> assertArrayNotHasKey ( 'emailCanonical' , $response [ 'body' ]);
$this -> assertArrayNotHasKey ( 'emailIsFree' , $response [ 'body' ]);
$this -> assertArrayNotHasKey ( 'emailIsDisposable' , $response [ 'body' ]);
$this -> assertArrayNotHasKey ( 'emailIsCorporate' , $response [ 'body' ]);
$this -> assertArrayNotHasKey ( 'emailIsCanonical' , $response [ 'body' ]);
2020-01-11 13:58:02 +00:00
/**
* Test for FAILURE
*/
2024-01-24 12:54:48 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
2020-02-17 07:16:11 +00:00
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2020-01-12 21:28:26 +00:00
]), [
2022-08-14 10:33:36 +00:00
'userId' => ID :: unique (),
2020-01-11 13:58:02 +00:00
'email' => $email ,
'password' => $password ,
'name' => $name ,
]);
$this -> assertEquals ( $response [ 'headers' ][ 'status-code' ], 409 );
2021-02-16 13:49:21 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
2022-08-14 10:33:36 +00:00
'userId' => ID :: unique (),
2021-02-16 13:49:21 +00:00
'email' => '' ,
'password' => '' ,
]);
2023-10-26 14:04:47 +00:00
$this -> assertEquals ( 400 , $response [ 'headers' ][ 'status-code' ]);
2021-02-16 13:49:21 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
2022-08-14 10:33:36 +00:00
'userId' => ID :: unique (),
2021-02-16 13:49:21 +00:00
'email' => $email ,
'password' => '' ,
]);
2023-10-26 14:04:47 +00:00
$this -> assertEquals ( 400 , $response [ 'headers' ][ 'status-code' ]);
2021-02-16 13:49:21 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2025-01-29 04:25:52 +00:00
'x-appwrite-dev-key' => $this -> getProject ()[ 'devKey' ] ? ? '' ,
2021-02-16 13:49:21 +00:00
]), [
2022-08-14 10:33:36 +00:00
'userId' => ID :: unique (),
2021-02-16 13:49:21 +00:00
'email' => '' ,
'password' => $password ,
]);
2023-10-26 14:04:47 +00:00
$this -> assertEquals ( 400 , $response [ 'headers' ][ 'status-code' ]);
2021-02-16 13:49:21 +00:00
2024-01-02 10:59:35 +00:00
$shortPassword = 'short' ;
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2024-12-15 10:49:35 +00:00
'x-appwrite-dev-key' => $this -> getProject ()[ 'devKey' ] ? ? ''
2024-01-02 10:59:35 +00:00
]), [
'userId' => ID :: unique (),
'email' => 'shortpass@appwrite.io' ,
'password' => $shortPassword
]);
2024-12-15 10:41:01 +00:00
$this -> assertEquals ( 400 , $response [ 'headers' ][ 'status-code' ]);
2024-01-02 10:59:35 +00:00
$longPassword = '' ;
for ( $i = 0 ; $i < 257 ; $i ++ ) { // 256 is the limit
$longPassword .= 'p' ;
}
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2024-12-15 10:49:35 +00:00
'x-appwrite-dev-key' => $this -> getProject ()[ 'devKey' ] ? ? ''
2024-01-02 10:59:35 +00:00
]), [
'userId' => ID :: unique (),
'email' => 'longpass@appwrite.io' ,
'password' => $longPassword ,
]);
2024-12-15 10:41:01 +00:00
$this -> assertEquals ( 400 , $response [ 'headers' ][ 'status-code' ]);
2024-01-02 10:59:35 +00:00
2020-01-11 13:58:02 +00:00
return [
2020-04-22 07:03:34 +00:00
'id' => $id ,
2020-01-11 13:58:02 +00:00
'email' => $email ,
'password' => $password ,
'name' => $name ,
];
}
2024-01-19 13:42:26 +00:00
public function testEmailOTPSession () : void
{
2025-09-18 08:39:42 +00:00
$isConsoleProject = $this -> getProject ()[ '$id' ] === 'console' ;
2026-02-09 01:20:42 +00:00
// Use unique email to avoid parallel test collisions
$otpEmail = 'otpuser-' . uniqid () . '@appwrite.io' ;
2024-01-19 13:42:26 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/tokens/email' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
'userId' => ID :: unique (),
2026-02-09 01:20:42 +00:00
'email' => $otpEmail
2024-01-19 13:42:26 +00:00
]);
2025-07-29 06:27:57 +00:00
$this -> assertEquals ( 201 , $response [ 'headers' ][ 'status-code' ]);
2024-01-19 13:42:26 +00:00
$this -> assertNotEmpty ( $response [ 'body' ][ '$id' ]);
$this -> assertNotEmpty ( $response [ 'body' ][ '$createdAt' ]);
$this -> assertNotEmpty ( $response [ 'body' ][ 'userId' ]);
$this -> assertNotEmpty ( $response [ 'body' ][ 'expire' ]);
$this -> assertEmpty ( $response [ 'body' ][ 'secret' ]);
2024-02-01 10:41:01 +00:00
$this -> assertEmpty ( $response [ 'body' ][ 'phrase' ]);
2024-01-19 13:42:26 +00:00
$userId = $response [ 'body' ][ 'userId' ];
2026-02-09 01:20:42 +00:00
$lastEmail = $this -> getLastEmailByAddress ( $otpEmail );
2025-07-24 14:09:23 +00:00
2026-02-09 01:20:42 +00:00
$this -> assertNotEmpty ( $lastEmail , 'Email not found for address: ' . $otpEmail );
2024-01-22 14:41:23 +00:00
$this -> assertEquals ( 'OTP for ' . $this -> getProject ()[ 'name' ] . ' Login' , $lastEmail [ 'subject' ]);
2024-01-19 13:42:26 +00:00
// FInd 6 concurrent digits in email text - OTP
preg_match_all ( " / \ b \ d { 6} \ b/ " , $lastEmail [ 'text' ], $matches );
chore: bump PHPStan to level 4 and fix all new errors
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:
- `app/controllers/general.php`: path-traversal guard was negating
`\substr(...)` before the strict comparison (`!\substr(...) === $base`
was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
`DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
result as an array with `deviceName/deviceBrand/deviceModel` keys.
Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
key was checking `$resourceKey === 'functions'` when `$resourceKey`
is `'functionId'|'siteId'` — always false. Switched to the intended
`$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
to `string|false` to match `openssl_encrypt`; this lets callers'
`=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
`array_key_exists('from', [])` branch in the Msg91 provider (empty
array literal; branch was unreachable).
Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
`markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
Installer State, `$source` on MigrationsWorker, `$account2` on two
GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
`assertNotNull` assertions with `addToAssertionCount(1)` or
`assertNotEmpty` where the runtime type was already known.
2026-04-19 12:01:20 +00:00
$code = $matches [ 0 ][ 0 ] ? ? '' ;
2024-01-19 13:42:26 +00:00
$this -> assertNotEmpty ( $code );
2025-07-24 14:09:23 +00:00
$this -> assertStringContainsStringIgnoringCase ( 'Use OTP ' . $code . ' to sign in to ' . $this -> getProject ()[ 'name' ] . '. Expires in 15 minutes.' , $lastEmail [ 'text' ]);
2024-01-19 13:42:26 +00:00
2025-09-18 08:39:42 +00:00
// Only Console project has branded logo in email.
if ( $isConsoleProject ) {
$this -> assertStringContainsStringIgnoringCase ( 'Appwrite logo' , $lastEmail [ 'html' ]);
2025-09-24 07:36:20 +00:00
} else {
$this -> assertStringNotContainsStringIgnoringCase ( 'Appwrite logo' , $lastEmail [ 'html' ]);
2025-09-18 08:39:42 +00:00
}
2024-01-19 13:42:26 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/sessions/token' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
'userId' => $userId ,
'secret' => $code
]);
$this -> assertEquals ( 201 , $response [ 'headers' ][ 'status-code' ]);
$this -> assertEquals ( $userId , $response [ 'body' ][ 'userId' ]);
$this -> assertNotEmpty ( $response [ 'body' ][ '$id' ]);
$this -> assertNotEmpty ( $response [ 'body' ][ 'expire' ]);
$this -> assertEmpty ( $response [ 'body' ][ 'secret' ]);
$session = $response [ 'cookies' ][ 'a_session_' . $this -> getProject ()[ '$id' ]];
$response = $this -> client -> call ( Client :: METHOD_GET , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
'cookie' => 'a_session_' . $this -> getProject ()[ '$id' ] . '=' . $session ,
]));
$this -> assertEquals ( 200 , $response [ 'headers' ][ 'status-code' ]);
$this -> assertEquals ( $userId , $response [ 'body' ][ '$id' ]);
2024-05-07 09:05:54 +00:00
$this -> assertEquals ( $userId , $response [ 'body' ][ '$id' ]);
$this -> assertTrue ( $response [ 'body' ][ 'emailVerification' ]);
2025-07-29 06:27:57 +00:00
$this -> assertArrayHasKey ( 'targets' , $response [ 'body' ]);
2026-02-09 01:20:42 +00:00
$this -> assertEquals ( $otpEmail , $response [ 'body' ][ 'targets' ][ 0 ][ 'identifier' ]);
2024-01-19 13:42:26 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/sessions/token' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
'userId' => $userId ,
'secret' => $code
]);
$this -> assertEquals ( 401 , $response [ 'headers' ][ 'status-code' ]);
$this -> assertEquals ( 'user_invalid_token' , $response [ 'body' ][ 'type' ]);
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/tokens/email' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
'userId' => ID :: unique (),
2026-02-09 01:20:42 +00:00
'email' => $otpEmail ,
2024-02-01 10:41:01 +00:00
'phrase' => true
2024-01-19 13:42:26 +00:00
]);
$this -> assertEquals ( $response [ 'headers' ][ 'status-code' ], 201 );
2024-02-01 10:41:01 +00:00
$this -> assertNotEmpty ( $response [ 'body' ][ 'phrase' ]);
2024-01-19 13:42:26 +00:00
$this -> assertEmpty ( $response [ 'body' ][ 'secret' ]);
$this -> assertEquals ( $userId , $response [ 'body' ][ 'userId' ]);
2024-02-01 14:13:30 +00:00
$phrase = $response [ 'body' ][ 'phrase' ];
2024-01-19 13:42:26 +00:00
2026-02-24 01:43:15 +00:00
$lastEmail = $this -> getLastEmailByAddress ( $otpEmail , function ( $email ) use ( $phrase ) {
$this -> assertStringContainsStringIgnoringCase ( 'security phrase' , $email [ 'text' ]);
$this -> assertStringContainsStringIgnoringCase ( $phrase , $email [ 'text' ]);
});
2026-02-09 01:20:42 +00:00
$this -> assertNotEmpty ( $lastEmail , 'Email not found for address: ' . $otpEmail );
2024-01-22 14:46:53 +00:00
$this -> assertEquals ( 'OTP for ' . $this -> getProject ()[ 'name' ] . ' Login' , $lastEmail [ 'subject' ]);
2024-01-19 13:42:26 +00:00
$this -> assertStringContainsStringIgnoringCase ( 'security phrase' , $lastEmail [ 'text' ]);
2024-02-01 14:13:30 +00:00
$this -> assertStringContainsStringIgnoringCase ( $phrase , $lastEmail [ 'text' ]);
2024-01-19 13:42:26 +00:00
2024-01-19 14:42:06 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/tokens/email' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
'userId' => ID :: unique (),
'email' => 'wrongemail'
]);
$this -> assertEquals ( 400 , $response [ 'headers' ][ 'status-code' ]);
$this -> assertEquals ( 'general_argument_invalid' , $response [ 'body' ][ 'type' ]);
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/tokens/email' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
'userId' => 'wrongId$' ,
'email' => 'email@appwrite.io'
]);
$this -> assertEquals ( 400 , $response [ 'headers' ][ 'status-code' ]);
$this -> assertEquals ( 'general_argument_invalid' , $response [ 'body' ][ 'type' ]);
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/tokens/email' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
'userId' => ID :: unique (),
]);
$this -> assertEquals ( 400 , $response [ 'headers' ][ 'status-code' ]);
$this -> assertEquals ( 'general_argument_invalid' , $response [ 'body' ][ 'type' ]);
2024-01-19 13:42:26 +00:00
}
2024-01-09 11:58:36 +00:00
public function testDeleteAccount () : void
{
2024-03-06 17:34:21 +00:00
$email = uniqid () . 'user@localhost.test' ;
$password = 'password' ;
$name = 'User Name' ;
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2024-12-15 10:49:35 +00:00
'x-appwrite-dev-key' => $this -> getProject ()[ 'devKey' ] ? ? ''
2024-03-06 17:34:21 +00:00
]), [
'userId' => ID :: unique (),
'email' => $email ,
'password' => $password ,
'name' => $name ,
]);
$this -> assertEquals ( $response [ 'headers' ][ 'status-code' ], 201 );
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/sessions/email' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
]), [
'email' => $email ,
'password' => $password ,
]);
$this -> assertEquals ( $response [ 'headers' ][ 'status-code' ], 201 );
$session = $response [ 'cookies' ][ 'a_session_' . $this -> getProject ()[ '$id' ]];
$response = $this -> client -> call ( Client :: METHOD_DELETE , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
'cookie' => 'a_session_' . $this -> getProject ()[ '$id' ] . '=' . $session ,
]));
$this -> assertEquals ( $response [ 'headers' ][ 'status-code' ], 204 );
2024-01-09 11:58:36 +00:00
}
2025-12-12 07:42:49 +00:00
2025-12-15 12:22:29 +00:00
public function testFallbackForTrustedIp () : void
2025-12-12 07:42:49 +00:00
{
$email = uniqid () . 'user@localhost.test' ;
$password = 'password' ;
$name = 'User Name' ;
2025-12-15 12:22:29 +00:00
// call appwrite directly to avoid proxy stripping the headers
$this -> client -> setEndpoint ( 'http://localhost/v1' );
2025-12-12 07:42:49 +00:00
$response = $this -> client -> call ( Client :: METHOD_POST , '/account' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2025-12-15 12:22:29 +00:00
'x-forwarded-for' => '191.0.113.195' ,
2025-12-12 07:42:49 +00:00
]), [
'userId' => ID :: unique (),
'email' => $email ,
'password' => $password ,
'name' => $name ,
]);
$this -> assertEquals ( $response [ 'headers' ][ 'status-code' ], 201 );
$response = $this -> client -> call ( Client :: METHOD_POST , '/account/sessions/email' , array_merge ([
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2025-12-15 12:22:29 +00:00
'x-forwarded-for' => '191.0.113.195' ,
2025-12-12 07:42:49 +00:00
]), [
'email' => $email ,
'password' => $password ,
]);
$this -> assertEquals ( $response [ 'headers' ][ 'status-code' ], 201 );
2025-12-15 12:22:29 +00:00
$this -> assertEquals ( '191.0.113.195' , $response [ 'body' ][ 'clientIp' ] ? ? $response [ 'body' ][ 'ip' ] ? ? '' );
2025-12-12 07:42:49 +00:00
}
2026-01-05 21:51:03 +00:00
2026-01-15 03:14:53 +00:00
#[Group('abuseEnabled')]
2026-01-05 21:51:03 +00:00
public function testAccountAbuseReset () : void
{
2026-01-13 09:01:22 +00:00
if ( System :: getEnv ( '_APP_OPTIONS_ABUSE' , 'enabled' ) === 'disabled' ) {
$this -> markTestSkipped ( 'Abuse checks are disabled.' );
}
$email = 'abuse.reset.' . bin2hex ( random_bytes ( 8 )) . '@example.com' ;
2026-01-05 21:51:03 +00:00
$password = 'password' ;
2026-01-13 09:01:22 +00:00
$abuseIp = '203.0.113.' . random_int ( 1 , 254 );
$baseHeaders = [
2026-01-05 21:51:03 +00:00
'origin' => 'http://localhost' ,
'content-type' => 'application/json' ,
'x-appwrite-project' => $this -> getProject ()[ '$id' ],
2026-01-13 09:01:22 +00:00
'x-forwarded-for' => $abuseIp ,
];
$account = $this -> client -> call ( Client :: METHOD_POST , '/account' , $baseHeaders , [
2026-01-05 21:51:03 +00:00
'userId' => ID :: unique (),
'email' => $email ,
'password' => $password ,
'name' => 'Abuse Reset Test' ,
]);
$this -> assertEquals ( $account [ 'headers' ][ 'status-code' ], 201 );
2026-01-06 14:18:22 +00:00
// 20 successful requests won't get blocked
2026-01-05 21:51:03 +00:00
for ( $i = 0 ; $i < 20 ; $i ++ ) {
2026-01-13 09:01:22 +00:00
$session = $this -> client -> call ( Client :: METHOD_POST , '/account/sessions/email' , $baseHeaders , [
2026-01-05 21:51:03 +00:00
'email' => $email ,
'password' => $password ,
]);
$this -> assertEquals ( $session [ 'headers' ][ 'status-code' ], 201 );
}
// 10 failures are OK
for ( $i = 0 ; $i < 10 ; $i ++ ) {
2026-01-13 09:01:22 +00:00
$session = $this -> client -> call ( Client :: METHOD_POST , '/account/sessions/email' , $baseHeaders , [
2026-01-05 21:51:03 +00:00
'email' => $email ,
'password' => 'wrongPassword' ,
]);
$this -> assertEquals ( $session [ 'headers' ][ 'status-code' ], 401 );
}
2026-01-13 09:01:22 +00:00
// Next failure(s) should be rate limited
$rateLimited = false ;
for ( $i = 0 ; $i < 10 ; $i ++ ) {
$session = $this -> client -> call ( Client :: METHOD_POST , '/account/sessions/email' , $baseHeaders , [
'email' => $email ,
'password' => 'wrongPassword' ,
]);
2026-01-05 21:51:03 +00:00
2026-01-13 09:01:22 +00:00
if ( $session [ 'headers' ][ 'status-code' ] === 429 ) {
$rateLimited = true ;
break ;
}
$this -> assertEquals ( $session [ 'headers' ][ 'status-code' ], 401 );
}
$this -> assertTrue ( $rateLimited , 'Expected a rate limited response after repeated failures.' );
2026-01-05 21:51:03 +00:00
2026-01-06 14:18:44 +00:00
// Even correct password is now blocked, correctness doesn't matter
2026-01-13 09:01:22 +00:00
$session = $this -> client -> call ( Client :: METHOD_POST , '/account/sessions/email' , $baseHeaders , [
2026-01-05 21:51:03 +00:00
'email' => $email ,
'password' => $password ,
]);
$this -> assertEquals ( $session [ 'headers' ][ 'status-code' ], 429 );
}
2022-06-02 12:47:07 +00:00
}