Merge pull request #11085 from appwrite/feat-success-abuse-reset

Feat: Abuse reset on success
This commit is contained in:
Matej Bačo 2026-01-06 15:56:36 +01:00 committed by GitHub
commit aa135d5cd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 189 additions and 77 deletions

View file

@ -223,7 +223,7 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots
- name: Failure Logs
if: failure()
@ -312,7 +312,7 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots
- name: Failure Logs
if: failure()
@ -322,8 +322,8 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_dev_keys:
name: E2E Service Test (Dev Keys)
e2e_abuse_enabled:
name: E2E Service Test (Abuse enabled)
runs-on: ubuntu-latest
needs: setup
steps:
@ -344,7 +344,7 @@ jobs:
docker compose up -d
sleep 30
- name: Run Projects tests with dev keys in dedicated table mode
- name: Run Projects tests in dedicated table mode
run: |
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
@ -354,7 +354,7 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
- name: Failure Logs
if: failure()
@ -364,8 +364,8 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_dev_keys_shared_mode:
name: E2E Shared Mode Service Test (Dev Keys)
e2e_abuse_enabled_shared_mode:
name: E2E Shared Mode Service Test (Abuse enabled)
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
@ -394,7 +394,7 @@ jobs:
docker compose up -d
sleep 30
- name: Run Projects tests with dev keys in ${{ matrix.tables-mode }} table mode
- name: Run Projects tests in ${{ matrix.tables-mode }} table mode
run: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
@ -410,7 +410,7 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
- name: Failure Logs
if: failure()
@ -420,7 +420,7 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_screenshots_keys:
e2e_screenshots:
name: E2E Service Test (Site Screenshots)
runs-on: ubuntu-latest
needs: setup

View file

@ -959,6 +959,7 @@ App::post('/v1/account/sessions/email')
))
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},email:{param-email}')
->label('abuse-reset', [201])
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('request')
@ -1257,6 +1258,7 @@ App::post('/v1/account/sessions/token')
))
->label('abuse-limit', 10)
->label('abuse-key', 'ip:{ip},userId:{param-userId}')
->label('abuse-reset', [201])
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('secret', '', new Text(256), 'Secret of a token generated by login methods. For example, the `createMagicURLToken` or `createPhoneToken` methods.')
->inject('request')
@ -2645,6 +2647,7 @@ App::put('/v1/account/sessions/magic-url')
))
->label('abuse-limit', 10)
->label('abuse-key', 'ip:{ip},userId:{param-userId}')
->label('abuse-reset', [201])
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')

View file

@ -814,7 +814,8 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
->inject('timelimit')
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, callable $timelimit) use ($parseLabel) {
$responsePayload = $response->getPayload();
@ -848,6 +849,41 @@ App::shutdown()
$route = $utopia->getRoute();
$requestParams = $route->getParamsValues();
/**
* Abuse labels
*/
$abuseEnabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
$abuseResetCode = $route->getLabel('abuse-reset', []);
$abuseResetCode = \is_array($abuseResetCode) ? $abuseResetCode : [$abuseResetCode];
if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) {
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (!empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$abuse->reset();
}
}
/**
* Audit labels
*/

View file

@ -45,7 +45,7 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.*",
"utopia-php/abuse": "1.*.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "2.0.2-rc1",
"utopia-php/auth": "0.5.*",
@ -64,7 +64,7 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.3.*",
"utopia-php/migration": "1.*.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",

108
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b873febd2b03c32ec61a57b690cc44a2",
"content-hash": "078c447eafec076507b5bc8b8c0198e7",
"packages": [
{
"name": "adhocore/jwt",
@ -69,16 +69,16 @@
},
{
"name": "appwrite/appwrite",
"version": "15.1.0",
"version": "19.1.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b"
"reference": "8738e812062f899c85b2598eef43d6a247f08a56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b",
"reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56",
"reference": "8738e812062f899c85b2598eef43d6a247f08a56",
"shasum": ""
},
"require": {
@ -87,7 +87,7 @@
"php": ">=7.1.0"
},
"require-dev": {
"mockery/mockery": "^1.6.6",
"mockery/mockery": "^1.6.12",
"phpunit/phpunit": "^10"
},
"type": "library",
@ -104,10 +104,10 @@
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0",
"source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0",
"url": "https://appwrite.io/support"
},
"time": "2025-08-01T04:50:51+00:00"
"time": "2025-12-18T08:07:43+00:00"
},
{
"name": "appwrite/php-clamav",
@ -3455,24 +3455,25 @@
},
{
"name": "utopia-php/abuse",
"version": "1.0.2",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828"
"reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/611fa66a97e87c0dbbc133a717d970da7a5ca828",
"reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/3339d057c6bb1fa3e5ac5b2598923f6938425ec2",
"reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2",
"shasum": ""
},
"require": {
"appwrite/appwrite": "19.*.*",
"ext-curl": "*",
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/database": "*"
"utopia-php/database": "3.*.*"
},
"require-dev": {
"laravel/pint": "1.*",
@ -3500,9 +3501,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/1.0.2"
"source": "https://github.com/utopia-php/abuse/tree/1.2.0"
},
"time": "2025-10-20T07:18:33+00:00"
"time": "2026-01-05T21:29:10+00:00"
},
{
"name": "utopia-php/analytics",
@ -4515,20 +4516,20 @@
},
{
"name": "utopia-php/migration",
"version": "1.3.9",
"version": "1.3.10",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "c55ec67c74663190cda10fd79297422147be7e85"
"reference": "cb357c42a5a5614605b546effbea1204ed64c6b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c55ec67c74663190cda10fd79297422147be7e85",
"reference": "c55ec67c74663190cda10fd79297422147be7e85",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/cb357c42a5a5614605b546effbea1204ed64c6b0",
"reference": "cb357c42a5a5614605b546effbea1204ed64c6b0",
"shasum": ""
},
"require": {
"appwrite/appwrite": "15.*",
"appwrite/appwrite": "19.*",
"ext-curl": "*",
"ext-openssl": "*",
"php": ">=8.1",
@ -4564,9 +4565,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.3.9"
"source": "https://github.com/utopia-php/migration/tree/1.3.10"
},
"time": "2025-12-08T08:45:09+00:00"
"time": "2026-01-06T10:47:11+00:00"
},
{
"name": "utopia-php/mongo",
@ -5438,16 +5439,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.8.6",
"version": "1.8.9",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "b6cc29d3bd247e193f3c06b4168dc69d884645f0"
"reference": "5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b6cc29d3bd247e193f3c06b4168dc69d884645f0",
"reference": "b6cc29d3bd247e193f3c06b4168dc69d884645f0",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1",
"reference": "5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1",
"shasum": ""
},
"require": {
@ -5483,9 +5484,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.6"
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.9"
},
"time": "2025-12-31T10:22:17+00:00"
"time": "2026-01-02T12:09:51+00:00"
},
{
"name": "doctrine/annotations",
@ -5566,30 +5567,29 @@
},
{
"name": "doctrine/instantiator",
"version": "2.0.0",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
"reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
"reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7",
"reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7",
"shasum": ""
},
"require": {
"php": "^8.1"
"php": "^8.4"
},
"require-dev": {
"doctrine/coding-standard": "^11",
"doctrine/coding-standard": "^14",
"ext-pdo": "*",
"ext-phar": "*",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "^1.9.4",
"phpstan/phpstan-phpunit": "^1.3",
"phpunit/phpunit": "^9.5.27",
"vimeo/psalm": "^5.4"
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5.58"
},
"type": "library",
"autoload": {
@ -5616,7 +5616,7 @@
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
"source": "https://github.com/doctrine/instantiator/tree/2.0.0"
"source": "https://github.com/doctrine/instantiator/tree/2.1.0"
},
"funding": [
{
@ -5632,7 +5632,7 @@
"type": "tidelift"
}
],
"time": "2022-12-30T00:23:10+00:00"
"time": "2026-01-05T06:47:08+00:00"
},
{
"name": "doctrine/lexer",
@ -5713,16 +5713,16 @@
},
{
"name": "laravel/pint",
"version": "v1.26.0",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
"reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
"url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
"reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
"shasum": ""
},
"require": {
@ -5733,9 +5733,9 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.90.0",
"illuminate/view": "^12.40.1",
"larastan/larastan": "^3.8.0",
"friendsofphp/php-cs-fixer": "^3.92.4",
"illuminate/view": "^12.44.0",
"larastan/larastan": "^3.8.1",
"laravel-zero/framework": "^12.0.4",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.3",
@ -5776,7 +5776,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-11-25T21:15:52+00:00"
"time": "2026-01-05T16:49:17+00:00"
},
{
"name": "matthiasmullie/minify",
@ -8562,16 +8562,16 @@
},
{
"name": "symfony/process",
"version": "v8.0.0",
"version": "v8.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149"
"reference": "0cbbd88ec836f8757641c651bb995335846abb78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149",
"reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149",
"url": "https://api.github.com/repos/symfony/process/zipball/0cbbd88ec836f8757641c651bb995335846abb78",
"reference": "0cbbd88ec836f8757641c651bb995335846abb78",
"shasum": ""
},
"require": {
@ -8603,7 +8603,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.0"
"source": "https://github.com/symfony/process/tree/v8.0.3"
},
"funding": [
{
@ -8623,7 +8623,7 @@
"type": "tidelift"
}
],
"time": "2025-10-16T16:25:44+00:00"
"time": "2025-12-19T10:01:18+00:00"
},
{
"name": "symfony/string",

View file

@ -363,4 +363,77 @@ trait AccountBase
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals('191.0.113.195', $response['body']['clientIp'] ?? $response['body']['ip'] ?? '');
}
/**
* @group abuseEnabled
*/
public function testAccountAbuseReset(): void
{
$email = \uniqid() . '.abuse.reset.test@example.com';
$password = 'password';
$account = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => 'Abuse Reset Test',
]);
$this->assertEquals($account['headers']['status-code'], 201);
// 20 successful requests won't get blocked
for ($i = 0; $i < 20; $i++) {
$session = $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($session['headers']['status-code'], 201);
}
// 10 failures are OK
for ($i = 0; $i < 10; $i++) {
$session = $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' => 'wrongPassword',
]);
$this->assertEquals($session['headers']['status-code'], 401);
}
// 11th request gets limited
$session = $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' => 'wrongPassword',
]);
$this->assertEquals($session['headers']['status-code'], 429);
// Even correct password is now blocked, correctness doesn't matter
$session = $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($session['headers']['status-code'], 429);
}
}

View file

@ -4783,7 +4783,7 @@ class ProjectsConsoleClientTest extends Scope
*/
/**
* @group devKeys
* @group abuseEnabled
*/
public function testCreateProjectDevKey(): void
{
@ -4844,7 +4844,7 @@ class ProjectsConsoleClientTest extends Scope
/**
* @group devKeys
* @group abuseEnabled
*/
public function testListProjectDevKey(): void
{
@ -4935,7 +4935,7 @@ class ProjectsConsoleClientTest extends Scope
/**
* @group devKeys
* @group abuseEnabled
*/
public function testGetProjectDevKey(): void
{
@ -4979,7 +4979,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testGetDevKeyWithSdks(): void
{
@ -5036,7 +5036,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testNoHostValidationWithDevKey(): void
{
@ -5117,7 +5117,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testCorsWithDevKey(): void
{
@ -5174,7 +5174,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testNoRateLimitWithDevKey(): void
{
@ -5279,7 +5279,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testUpdateProjectDevKey(): void
{
@ -5324,7 +5324,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testDeleteProjectDevKey(): void
{