mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
2927 lines
112 KiB
PHP
2927 lines
112 KiB
PHP
<?php
|
|
|
|
namespace Tests\E2E\Services\Project;
|
|
|
|
use PHPUnit\Framework\Attributes\Before;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use Tests\E2E\Client;
|
|
use Utopia\Database\Query;
|
|
|
|
trait OAuth2Base
|
|
{
|
|
/**
|
|
* Reset providers we mutate in tests back to a known empty/disabled state.
|
|
* The ProjectCustom trait reuses the same project across tests in a class,
|
|
* and the OAuth2 PATCH endpoint is additive (omitted fields are preserved),
|
|
* so without a reset state would leak between tests.
|
|
*
|
|
* Assert on the reset response so a silently broken reset (e.g. validation
|
|
* change) surfaces immediately rather than corrupting downstream tests.
|
|
*/
|
|
#[Before(priority: -1)]
|
|
protected function resetProjectOAuth2(): void
|
|
{
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(
|
|
200,
|
|
$response['headers']['status-code'],
|
|
'OAuth2 reset failed — downstream tests will be unreliable. Body: ' . \json_encode($response['body'] ?? null),
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// List OAuth2 providers
|
|
// =========================================================================
|
|
|
|
public function testListOAuth2Providers(): void
|
|
{
|
|
$response = $this->listOAuth2Providers();
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertArrayHasKey('total', $response['body']);
|
|
$this->assertArrayHasKey('providers', $response['body']);
|
|
$this->assertGreaterThan(0, $response['body']['total']);
|
|
$this->assertSame($response['body']['total'], \count($response['body']['providers']));
|
|
}
|
|
|
|
public function testListOAuth2ProvidersIncludesKnownProviders(): void
|
|
{
|
|
$response = $this->listOAuth2Providers();
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
|
|
$ids = \array_column($response['body']['providers'], '$id');
|
|
|
|
// Spot-check a representative cross-section of providers across all
|
|
// provider shapes (plain, multi-field, sandboxed, custom param names).
|
|
$expected = [
|
|
'github',
|
|
'amazon',
|
|
'apple',
|
|
'auth0',
|
|
'authentik',
|
|
'fusionauth',
|
|
'gitlab',
|
|
'keycloak',
|
|
'oidc',
|
|
'okta',
|
|
'microsoft',
|
|
'dropbox',
|
|
'paypalSandbox',
|
|
'kick',
|
|
];
|
|
|
|
foreach ($expected as $providerId) {
|
|
$this->assertContains($providerId, $ids, "Missing provider {$providerId} in listOAuth2Providers response");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pin the exact set of registered providers — adding or removing a
|
|
* provider must be a deliberate change to this assertion. Catches
|
|
* registration drift (e.g. forgetting to wire a new provider into
|
|
* `Base::getProviderActions()`).
|
|
*/
|
|
public function testListOAuth2ProvidersExposesEntireRegistry(): void
|
|
{
|
|
$response = $this->listOAuth2Providers();
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
|
|
$ids = \array_column($response['body']['providers'], '$id');
|
|
\sort($ids);
|
|
|
|
$expected = [
|
|
'amazon', 'apple', 'auth0', 'authentik', 'autodesk', 'bitbucket',
|
|
'bitly', 'box', 'dailymotion', 'discord', 'disqus', 'dropbox',
|
|
'etsy', 'facebook', 'figma', 'fusionauth', 'github', 'gitlab',
|
|
'google', 'keycloak', 'kick', 'linkedin', 'microsoft', 'notion',
|
|
'oidc', 'okta', 'paypal', 'paypalSandbox', 'podio', 'salesforce',
|
|
'slack', 'spotify', 'stripe', 'tradeshift', 'tradeshiftBox',
|
|
'twitch', 'wordpress', 'x', 'yahoo', 'yandex', 'zoho', 'zoom',
|
|
];
|
|
\sort($expected);
|
|
|
|
$this->assertSame($expected, $ids, 'Registry drift — listed providers do not match the expected set.');
|
|
}
|
|
|
|
public function testListOAuth2ProvidersResponseShape(): void
|
|
{
|
|
$response = $this->listOAuth2Providers();
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
|
|
foreach ($response['body']['providers'] as $provider) {
|
|
$this->assertArrayHasKey('$id', $provider);
|
|
$this->assertArrayHasKey('enabled', $provider);
|
|
$this->assertIsString($provider['$id']);
|
|
$this->assertIsBool($provider['enabled']);
|
|
}
|
|
}
|
|
|
|
public function testListOAuth2ProvidersClientSecretsNotExposed(): void
|
|
{
|
|
// Seed credentials so the list cannot trivially return empty values.
|
|
$this->updateOAuth2('amazon', [
|
|
'clientId' => 'amzn1.application-oa2-client.testListSeed',
|
|
'clientSecret' => 'super-secret-must-not-leak',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->listOAuth2Providers();
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
|
|
$matched = false;
|
|
foreach ($response['body']['providers'] as $provider) {
|
|
if ($provider['$id'] !== 'amazon') {
|
|
continue;
|
|
}
|
|
|
|
$matched = true;
|
|
$this->assertSame('amzn1.application-oa2-client.testListSeed', $provider['clientId']);
|
|
$this->assertSame('', $provider['clientSecret']);
|
|
}
|
|
|
|
$this->assertTrue($matched, 'List did not include the seeded provider.');
|
|
}
|
|
|
|
public function testListOAuth2ProvidersWithoutAuthentication(): void
|
|
{
|
|
$response = $this->listOAuth2Providers(authenticated: false);
|
|
|
|
$this->assertSame(401, $response['headers']['status-code']);
|
|
}
|
|
|
|
public function testListOAuth2ProvidersExcludesUnregisteredConfigEntries(): void
|
|
{
|
|
// `mock` and `mock-unverified` exist in oAuthProviders config (enabled: true)
|
|
// but are intentionally absent from Base::getProviderActions() — they're
|
|
// internal Mock OAuth2 adapters used by other test suites, not public
|
|
// providers. XList iterates the action registry, so they must never be
|
|
// included even though config marks them enabled.
|
|
$response = $this->listOAuth2Providers();
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
|
|
$ids = \array_column($response['body']['providers'], '$id');
|
|
$this->assertNotContains('mock', $ids);
|
|
$this->assertNotContains('mock-unverified', $ids);
|
|
}
|
|
|
|
public function testListOAuth2ProvidersTotalFalse(): void
|
|
{
|
|
$response = $this->listOAuth2Providers(total: false);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame(0, $response['body']['total']);
|
|
$this->assertGreaterThan(0, \count($response['body']['providers']));
|
|
}
|
|
|
|
public function testListOAuth2ProvidersWithLimit(): void
|
|
{
|
|
$response = $this->listOAuth2Providers([
|
|
Query::limit(1)->toString(),
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertCount(1, $response['body']['providers']);
|
|
$this->assertGreaterThan(1, $response['body']['total']);
|
|
}
|
|
|
|
public function testListOAuth2ProvidersWithOffset(): void
|
|
{
|
|
$listAll = $this->listOAuth2Providers();
|
|
$this->assertSame(200, $listAll['headers']['status-code']);
|
|
|
|
$listOffset = $this->listOAuth2Providers([
|
|
Query::offset(1)->toString(),
|
|
]);
|
|
|
|
$this->assertSame(200, $listOffset['headers']['status-code']);
|
|
$this->assertCount(\count($listAll['body']['providers']) - 1, $listOffset['body']['providers']);
|
|
$this->assertSame($listAll['body']['total'], $listOffset['body']['total']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Get OAuth2 provider
|
|
// =========================================================================
|
|
|
|
public function testGetOAuth2Provider(): void
|
|
{
|
|
$response = $this->getOAuth2Provider('github');
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('github', $response['body']['$id']);
|
|
$this->assertArrayHasKey('enabled', $response['body']);
|
|
$this->assertArrayHasKey('clientId', $response['body']);
|
|
$this->assertArrayHasKey('clientSecret', $response['body']);
|
|
$this->assertSame('', $response['body']['clientSecret']);
|
|
}
|
|
|
|
public function testGetOAuth2ProviderWithAlias(): void
|
|
{
|
|
// The action declares the canonical param name as `providerId` and
|
|
// registers `provider` as an alias so that older SDK versions that
|
|
// send the provider in the query string continue to work.
|
|
$headers = [
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
];
|
|
$headers = \array_merge($headers, $this->getHeaders());
|
|
|
|
// Call with `provider` in query string (legacy behaviour)
|
|
$response = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/project/oauth2/github?provider=github',
|
|
$headers,
|
|
);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('github', $response['body']['$id']);
|
|
}
|
|
|
|
public function testGetOAuth2ProviderClientSecretWriteOnly(): void
|
|
{
|
|
$this->updateOAuth2('amazon', [
|
|
'clientId' => 'amzn1.application-oa2-client.getSecretCheck',
|
|
'clientSecret' => 'must-never-be-returned',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->getOAuth2Provider('amazon');
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('amzn1.application-oa2-client.getSecretCheck', $response['body']['clientId']);
|
|
$this->assertSame('', $response['body']['clientSecret']);
|
|
}
|
|
|
|
public function testGetOAuth2ProviderMatchesListEntry(): void
|
|
{
|
|
$list = $this->listOAuth2Providers();
|
|
$this->assertSame(200, $list['headers']['status-code']);
|
|
|
|
// Drive the loop directly off the LIST result so any provider added
|
|
// to the registry is automatically checked for List/Get parity.
|
|
foreach ($list['body']['providers'] as $listEntry) {
|
|
$providerId = $listEntry['$id'];
|
|
$get = $this->getOAuth2Provider($providerId);
|
|
|
|
$this->assertSame(200, $get['headers']['status-code'], "GET failed for {$providerId}");
|
|
$this->assertSame($listEntry, $get['body'], "List/Get drift on {$providerId}");
|
|
}
|
|
}
|
|
|
|
public function testGetOAuth2ProviderUnsupported(): void
|
|
{
|
|
// The `providerId` param is validated by a WhiteList of registered
|
|
// OAuth2 provider keys, so an unknown value is rejected at validation
|
|
// time — before the action runs — and surfaces as a generic argument
|
|
// error rather than `project_provider_unsupported`.
|
|
$response = $this->getOAuth2Provider('not-a-real-provider');
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testGetOAuth2ProviderRegisteredInConfigButNoUpdateClass(): void
|
|
{
|
|
// `mock` is present in oAuthProviders config (enabled: true) but is
|
|
// NOT registered in Base::getProviderActions(). It passes the
|
|
// WhiteList validator (which only checks config membership) and
|
|
// reaches the action body, where the action-registry check throws
|
|
// `project_provider_unsupported`.
|
|
$response = $this->getOAuth2Provider('mock');
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('project_provider_unsupported', $response['body']['type']);
|
|
}
|
|
|
|
public function testGetOAuth2ProviderWithoutAuthentication(): void
|
|
{
|
|
$response = $this->getOAuth2Provider('github', authenticated: false);
|
|
|
|
$this->assertSame(401, $response['headers']['status-code']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update plain provider (Amazon — clientId + clientSecret, no extra fields)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2Plain(): void
|
|
{
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'clientId' => 'amzn1.application-oa2-client.test01',
|
|
'clientSecret' => 'test-secret-01',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('amazon', $response['body']['$id']);
|
|
$this->assertSame('amzn1.application-oa2-client.test01', $response['body']['clientId']);
|
|
$this->assertSame(false, $response['body']['enabled']);
|
|
}
|
|
|
|
public function testUpdateOAuth2PlainEnable(): void
|
|
{
|
|
// Amazon has no verifyCredentials() hook, so enabling with arbitrary
|
|
// credentials succeeds without making a real network call.
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'clientId' => 'amzn1.application-oa2-client.test02',
|
|
'clientSecret' => 'test-secret-02',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame(true, $response['body']['enabled']);
|
|
}
|
|
|
|
public function testUpdateOAuth2PlainDisable(): void
|
|
{
|
|
$this->updateOAuth2('amazon', [
|
|
'clientId' => 'amzn1.application-oa2-client.test03',
|
|
'clientSecret' => 'test-secret-03',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame(false, $response['body']['enabled']);
|
|
// Credentials persist across an enabled toggle.
|
|
$this->assertSame('amzn1.application-oa2-client.test03', $response['body']['clientId']);
|
|
}
|
|
|
|
public function testUpdateOAuth2PlainPartial(): void
|
|
{
|
|
// Seed both credentials.
|
|
$this->updateOAuth2('amazon', [
|
|
'clientId' => 'seed-client-id',
|
|
'clientSecret' => 'seed-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Patch only clientId.
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'clientId' => 'updated-client-id',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('updated-client-id', $response['body']['clientId']);
|
|
|
|
// Read back through GET to confirm the secret is still set internally
|
|
// (write-only, so we cannot inspect the value, but enabling should still
|
|
// succeed because the secret remains non-empty).
|
|
$enable = $this->updateOAuth2('amazon', [
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertSame(true, $enable['body']['enabled']);
|
|
}
|
|
|
|
public function testUpdateOAuth2PlainEnableRequiresCredentials(): void
|
|
{
|
|
// Start from a clean state with no credentials.
|
|
$this->updateOAuth2('amazon', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2PlainEnabledOmittedDoesNotThrow(): void
|
|
{
|
|
// With enabled omitted (null) and no credentials, the silent-validation
|
|
// branch must not surface as an error.
|
|
$this->updateOAuth2('amazon', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'clientId' => 'partial-only',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame(false, $response['body']['enabled']);
|
|
$this->assertSame('partial-only', $response['body']['clientId']);
|
|
}
|
|
|
|
public function testUpdateOAuth2PlainResponseModel(): void
|
|
{
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'clientId' => 'amzn1.application-oa2-client.modelCheck',
|
|
'clientSecret' => 'model-check-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertArrayHasKey('$id', $response['body']);
|
|
$this->assertArrayHasKey('enabled', $response['body']);
|
|
$this->assertArrayHasKey('clientId', $response['body']);
|
|
$this->assertArrayHasKey('clientSecret', $response['body']);
|
|
}
|
|
|
|
public function testUpdateOAuth2WithoutAuthentication(): void
|
|
{
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'clientId' => 'no-auth',
|
|
'clientSecret' => 'no-auth',
|
|
'enabled' => false,
|
|
], authenticated: false);
|
|
|
|
$this->assertSame(401, $response['headers']['status-code']);
|
|
}
|
|
|
|
public function testUpdateOAuth2UnknownProvider(): void
|
|
{
|
|
// Each Update endpoint is registered at a fixed `/oauth2/{providerId}`
|
|
// path, so an unknown provider does not match any route → 404.
|
|
$response = $this->updateOAuth2('not-a-real-provider', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(404, $response['headers']['status-code']);
|
|
}
|
|
|
|
public function testUpdateOAuth2InvalidEnabled(): void
|
|
{
|
|
$response = $this->updateOAuth2('amazon', [
|
|
'enabled' => 'not-a-boolean',
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update GitHub (verifyCredentials makes a real call to GitHub on enable)
|
|
//
|
|
// Only failure paths and the silent-on-disable branch are tested here.
|
|
// Happy-path enable would require real GitHub OAuth2 credentials, which
|
|
// CI doesn't have. Wiring, validation, and the non-enabling branch are
|
|
// sufficient to surface most regressions; success-path issues are caught
|
|
// by integration / staging environments instead.
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2GitHubInvalidCredentialsRejected(): void
|
|
{
|
|
// GitHub is the only provider with a real verifyCredentials() hook.
|
|
// Enabling with bogus credentials must surface a 400 from the wrapping
|
|
// exception, not silently succeed.
|
|
$response = $this->updateOAuth2('github', [
|
|
'clientId' => 'fake-client-id-' . \uniqid(),
|
|
'clientSecret' => 'fake-client-secret',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
|
|
// Cleanup: ensure it's left disabled.
|
|
$this->updateOAuth2('github', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2GitHubInvalidCredentialsSilentWhenNotEnabling(): void
|
|
{
|
|
// When `enabled` is omitted, verifyCredentials() failure is swallowed.
|
|
// The provider remains disabled but the request succeeds.
|
|
$response = $this->updateOAuth2('github', [
|
|
'clientId' => 'still-fake-' . \uniqid(),
|
|
'clientSecret' => 'still-fake-secret',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame(false, $response['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('github', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Apple (serviceId + keyId + teamId + p8File)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2Apple(): void
|
|
{
|
|
$response = $this->updateOAuth2('apple', [
|
|
'serviceId' => 'ip.appwrite.app.web',
|
|
'keyId' => 'P4000000N8',
|
|
'teamId' => 'D4000000R6',
|
|
'p8File' => '-----BEGIN PRIVATE KEY-----TEST-----END PRIVATE KEY-----',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('apple', $response['body']['$id']);
|
|
$this->assertSame('ip.appwrite.app.web', $response['body']['serviceId']);
|
|
$this->assertSame('P4000000N8', $response['body']['keyId']);
|
|
$this->assertSame('D4000000R6', $response['body']['teamId']);
|
|
$this->assertSame('', $response['body']['p8File']);
|
|
$this->assertSame(false, $response['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => '',
|
|
'keyId' => '',
|
|
'teamId' => '',
|
|
'p8File' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2ApplePartial(): void
|
|
{
|
|
// Seed all four fields.
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => 'ip.appwrite.app.seed',
|
|
'keyId' => 'KEYSEED01',
|
|
'teamId' => 'TEAMSEED01',
|
|
'p8File' => '-----BEGIN PRIVATE KEY-----SEED-----END PRIVATE KEY-----',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Patch only `keyId` — others must be preserved.
|
|
$response = $this->updateOAuth2('apple', [
|
|
'keyId' => 'KEYUPDATED',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('ip.appwrite.app.seed', $response['body']['serviceId']);
|
|
$this->assertSame('KEYUPDATED', $response['body']['keyId']);
|
|
$this->assertSame('TEAMSEED01', $response['body']['teamId']);
|
|
$this->assertSame('', $response['body']['p8File']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => '',
|
|
'keyId' => '',
|
|
'teamId' => '',
|
|
'p8File' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2ApplePartialPreservesEachField(): void
|
|
{
|
|
// Seed all four fields, then patch each one individually and confirm
|
|
// the others survive across the chain. testUpdateOAuth2ApplePartial
|
|
// only covers `keyId`; this exercises serviceId/teamId/p8File too.
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => 'ip.appwrite.app.merge',
|
|
'keyId' => 'KEYMERGE01',
|
|
'teamId' => 'TEAMMERGE',
|
|
'p8File' => '-----BEGIN PRIVATE KEY-----MERGE-----END PRIVATE KEY-----',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Patch only `teamId`.
|
|
$teamOnly = $this->updateOAuth2('apple', [
|
|
'teamId' => 'TEAMROTATED',
|
|
]);
|
|
$this->assertSame(200, $teamOnly['headers']['status-code']);
|
|
$this->assertSame('TEAMROTATED', $teamOnly['body']['teamId']);
|
|
$this->assertSame('KEYMERGE01', $teamOnly['body']['keyId']);
|
|
$this->assertSame('', $teamOnly['body']['p8File']);
|
|
$this->assertSame('ip.appwrite.app.merge', $teamOnly['body']['serviceId']);
|
|
|
|
// Patch only `serviceId` — keyId/teamId/p8File live in the JSON blob
|
|
// and must survive a top-level (non-blob) field update.
|
|
$serviceOnly = $this->updateOAuth2('apple', [
|
|
'serviceId' => 'ip.appwrite.app.rotated',
|
|
]);
|
|
$this->assertSame(200, $serviceOnly['headers']['status-code']);
|
|
$this->assertSame('ip.appwrite.app.rotated', $serviceOnly['body']['serviceId']);
|
|
|
|
// Patch only `p8File`. keyId/teamId/serviceId must still be set
|
|
// internally — confirm by enabling. Apple has no verifyCredentials()
|
|
// hook, so persistCredentials only checks for non-empty serviceId and
|
|
// non-empty stored secret blob.
|
|
$p8Only = $this->updateOAuth2('apple', [
|
|
'p8File' => '-----BEGIN PRIVATE KEY-----ROTATED-----END PRIVATE KEY-----',
|
|
]);
|
|
$this->assertSame(200, $p8Only['headers']['status-code']);
|
|
|
|
$enable = $this->updateOAuth2('apple', ['enabled' => true]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertTrue($enable['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => '',
|
|
'keyId' => '',
|
|
'teamId' => '',
|
|
'p8File' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2AppleClearAllFieldsBlocksEnable(): void
|
|
{
|
|
// Seed all four Apple fields.
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => 'ip.appwrite.app.clearAll',
|
|
'keyId' => 'KEYCLEARALL',
|
|
'teamId' => 'TEAMCLEARALL',
|
|
'p8File' => '-----BEGIN PRIVATE KEY-----CLEARALL-----END PRIVATE KEY-----',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Clear all credentials with empty strings. With `enabled` omitted, the
|
|
// silent-validation branch swallows the empty-credentials throw, so the
|
|
// call still succeeds — see testUpdateOAuth2PlainEnabledOmittedDoesNotThrow.
|
|
$clear = $this->updateOAuth2('apple', [
|
|
'serviceId' => '',
|
|
'keyId' => '',
|
|
'teamId' => '',
|
|
'p8File' => '',
|
|
]);
|
|
$this->assertSame(200, $clear['headers']['status-code']);
|
|
$this->assertSame('', $clear['body']['serviceId']);
|
|
|
|
// A subsequent `enabled => true` must now 400. Empty serviceId trips
|
|
// persistCredentials' empty(appId) guard before any provider hook runs,
|
|
// proving that the clear actually took effect on stored state.
|
|
$enable = $this->updateOAuth2('apple', [
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(400, $enable['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $enable['body']['type']);
|
|
|
|
// Cleanup (already cleared; included for reset symmetry).
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => '',
|
|
'keyId' => '',
|
|
'teamId' => '',
|
|
'p8File' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2AppleResponseModel(): void
|
|
{
|
|
$response = $this->updateOAuth2('apple', [
|
|
'serviceId' => 'ip.appwrite.app.shape',
|
|
'keyId' => 'SHAPEKEY01',
|
|
'teamId' => 'SHAPETEAM',
|
|
'p8File' => '-----BEGIN PRIVATE KEY-----SHAPE-----END PRIVATE KEY-----',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertArrayHasKey('$id', $response['body']);
|
|
$this->assertArrayHasKey('enabled', $response['body']);
|
|
$this->assertArrayHasKey('serviceId', $response['body']);
|
|
$this->assertArrayHasKey('keyId', $response['body']);
|
|
$this->assertArrayHasKey('teamId', $response['body']);
|
|
$this->assertArrayHasKey('p8File', $response['body']);
|
|
// Apple has no clientId/clientSecret in the response model.
|
|
$this->assertArrayNotHasKey('clientId', $response['body']);
|
|
$this->assertArrayNotHasKey('clientSecret', $response['body']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => '',
|
|
'keyId' => '',
|
|
'teamId' => '',
|
|
'p8File' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testGetOAuth2AppleSecretsWriteOnly(): void
|
|
{
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => 'ip.appwrite.app.read',
|
|
'keyId' => 'KEYREAD',
|
|
'teamId' => 'TEAMREAD',
|
|
'p8File' => '-----BEGIN PRIVATE KEY-----READ-----END PRIVATE KEY-----',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->getOAuth2Provider('apple');
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('ip.appwrite.app.read', $response['body']['serviceId']);
|
|
$this->assertSame('KEYREAD', $response['body']['keyId']);
|
|
$this->assertSame('TEAMREAD', $response['body']['teamId']);
|
|
$this->assertSame('', $response['body']['p8File']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => '',
|
|
'keyId' => '',
|
|
'teamId' => '',
|
|
'p8File' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2AppleEnableAndReadBack(): void
|
|
{
|
|
// Apple has no verifyCredentials() hook, so enabling with arbitrary
|
|
// (well-formed) values succeeds without any real Apple network call.
|
|
$update = $this->updateOAuth2('apple', [
|
|
'serviceId' => 'ip.appwrite.app.enable',
|
|
'keyId' => 'ENABLEKEY',
|
|
'teamId' => 'ENABLETEAM',
|
|
'p8File' => '-----BEGIN PRIVATE KEY-----ENABLE-----END PRIVATE KEY-----',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide p8File while keeping the non-secret fields.
|
|
$get = $this->getOAuth2Provider('apple');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('ip.appwrite.app.enable', $get['body']['serviceId']);
|
|
$this->assertSame('ENABLEKEY', $get['body']['keyId']);
|
|
$this->assertSame('ENABLETEAM', $get['body']['teamId']);
|
|
$this->assertSame('', $get['body']['p8File']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('apple', [
|
|
'serviceId' => '',
|
|
'keyId' => '',
|
|
'teamId' => '',
|
|
'p8File' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Auth0 (clientId + clientSecret + optional endpoint)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2Auth0(): void
|
|
{
|
|
$response = $this->updateOAuth2('auth0', [
|
|
'clientId' => 'OaOkIA000000000000000000005KLSYq',
|
|
'clientSecret' => 'auth0-test-secret',
|
|
'endpoint' => 'example.us.auth0.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('auth0', $response['body']['$id']);
|
|
$this->assertSame('OaOkIA000000000000000000005KLSYq', $response['body']['clientId']);
|
|
$this->assertSame('example.us.auth0.com', $response['body']['endpoint']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('auth0', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2Auth0PartialEndpoint(): void
|
|
{
|
|
// Seed clientSecret + endpoint.
|
|
$this->updateOAuth2('auth0', [
|
|
'clientId' => 'auth0-seed-client',
|
|
'clientSecret' => 'auth0-seed-secret',
|
|
'endpoint' => 'seed.us.auth0.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Update only endpoint.
|
|
$response = $this->updateOAuth2('auth0', [
|
|
'endpoint' => 'updated.us.auth0.com',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('updated.us.auth0.com', $response['body']['endpoint']);
|
|
// clientId is unchanged on top-level provider state.
|
|
$this->assertSame('auth0-seed-client', $response['body']['clientId']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('auth0', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2Auth0PartialPreservesEachField(): void
|
|
{
|
|
// testUpdateOAuth2Auth0PartialEndpoint only patches `endpoint`. Cover
|
|
// patching `clientSecret` alone (must not wipe endpoint) and `clientId`
|
|
// alone (must not wipe the JSON-blob fields).
|
|
$this->updateOAuth2('auth0', [
|
|
'clientId' => 'auth0-merge-client',
|
|
'clientSecret' => 'auth0-merge-secret',
|
|
'endpoint' => 'merge.us.auth0.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Patch only clientSecret — clientId and endpoint must survive.
|
|
$secretOnly = $this->updateOAuth2('auth0', [
|
|
'clientSecret' => 'auth0-rotated-secret',
|
|
]);
|
|
$this->assertSame(200, $secretOnly['headers']['status-code']);
|
|
$this->assertSame('auth0-merge-client', $secretOnly['body']['clientId']);
|
|
$this->assertSame('merge.us.auth0.com', $secretOnly['body']['endpoint']);
|
|
|
|
// Patch only clientId — endpoint must survive.
|
|
$idOnly = $this->updateOAuth2('auth0', [
|
|
'clientId' => 'auth0-rotated-client',
|
|
]);
|
|
$this->assertSame(200, $idOnly['headers']['status-code']);
|
|
$this->assertSame('auth0-rotated-client', $idOnly['body']['clientId']);
|
|
$this->assertSame('merge.us.auth0.com', $idOnly['body']['endpoint']);
|
|
|
|
// Confirm the rotated clientSecret survived the chain by enabling.
|
|
// Auth0 has no verifyCredentials() hook; non-empty secret is enough.
|
|
$enable = $this->updateOAuth2('auth0', ['enabled' => true]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertTrue($enable['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('auth0', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2Auth0EndpointAcceptsEmpty(): void
|
|
{
|
|
// Auth0's `endpoint` validator is `Nullable(Text(256, 0))`. Passing
|
|
// `''` must clear the stored value rather than leave it untouched
|
|
// (would happen if the merge fell back to existing on empty-string).
|
|
$this->updateOAuth2('auth0', [
|
|
'clientId' => 'auth0-clear-client',
|
|
'clientSecret' => 'auth0-clear-secret',
|
|
'endpoint' => 'before.us.auth0.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('auth0', [
|
|
'endpoint' => '',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('', $response['body']['endpoint']);
|
|
$this->assertSame('auth0-clear-client', $response['body']['clientId']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('auth0', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2Auth0EnableAndReadBack(): void
|
|
{
|
|
$update = $this->updateOAuth2('auth0', [
|
|
'clientId' => 'auth0-enable-client',
|
|
'clientSecret' => 'auth0-enable-secret',
|
|
'endpoint' => 'enable.us.auth0.com',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide clientSecret while keeping clientId and endpoint.
|
|
$get = $this->getOAuth2Provider('auth0');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('auth0-enable-client', $get['body']['clientId']);
|
|
$this->assertSame('enable.us.auth0.com', $get['body']['endpoint']);
|
|
$this->assertSame('', $get['body']['clientSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('auth0', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Authentik (clientId + clientSecret + optional endpoint)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2AuthentikAllowsOmittedEndpointWhenDisabled(): void
|
|
{
|
|
$response = $this->updateOAuth2('authentik', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('authentik', $response['body']['$id']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('authentik', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2AuthentikEmptyEndpointRejectedWhenEnabling(): void
|
|
{
|
|
$response = $this->updateOAuth2('authentik', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'endpoint' => '',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2Authentik(): void
|
|
{
|
|
$response = $this->updateOAuth2('authentik', [
|
|
'clientId' => 'dTKOPa0000000000000000000000000000e7G8hv',
|
|
'clientSecret' => 'authentik-secret',
|
|
'endpoint' => 'example.authentik.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('authentik', $response['body']['$id']);
|
|
$this->assertSame('dTKOPa0000000000000000000000000000e7G8hv', $response['body']['clientId']);
|
|
$this->assertSame('example.authentik.com', $response['body']['endpoint']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('authentik', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2AuthentikPartialPreservesSecret(): void
|
|
{
|
|
// The `clientSecret` and `endpoint` live in the JSON blob and must
|
|
// survive when omitted on a subsequent call that only changes clientId.
|
|
$this->updateOAuth2('authentik', [
|
|
'clientId' => 'authentik-merge-client',
|
|
'clientSecret' => 'authentik-merge-secret',
|
|
'endpoint' => 'merge.authentik.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('authentik', [
|
|
'clientId' => 'authentik-rotated-client',
|
|
]);
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('authentik-rotated-client', $response['body']['clientId']);
|
|
$this->assertSame('merge.authentik.com', $response['body']['endpoint']);
|
|
|
|
// Confirm clientSecret survived the omitted-field merge by enabling
|
|
// without re-sending endpoint.
|
|
$enable = $this->updateOAuth2('authentik', [
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertTrue($enable['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('authentik', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2AuthentikEnableAndReadBack(): void
|
|
{
|
|
$update = $this->updateOAuth2('authentik', [
|
|
'clientId' => 'authentik-enable-client',
|
|
'clientSecret' => 'authentik-enable-secret',
|
|
'endpoint' => 'enable.authentik.com',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide clientSecret while keeping clientId and endpoint.
|
|
$get = $this->getOAuth2Provider('authentik');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('authentik-enable-client', $get['body']['clientId']);
|
|
$this->assertSame('enable.authentik.com', $get['body']['endpoint']);
|
|
$this->assertSame('', $get['body']['clientSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('authentik', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update FusionAuth (clientId + clientSecret + optional endpoint)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2FusionAuthAllowsOmittedEndpointWhenDisabled(): void
|
|
{
|
|
$response = $this->updateOAuth2('fusionauth', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('fusionauth', $response['body']['$id']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('fusionauth', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2FusionAuthEmptyEndpointRejectedWhenEnabling(): void
|
|
{
|
|
$response = $this->updateOAuth2('fusionauth', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'endpoint' => '',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2FusionAuth(): void
|
|
{
|
|
$response = $this->updateOAuth2('fusionauth', [
|
|
'clientId' => 'b2222c00-0000-0000-0000-000000862097',
|
|
'clientSecret' => 'fusionauth-secret',
|
|
'endpoint' => 'example.fusionauth.io',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('fusionauth', $response['body']['$id']);
|
|
$this->assertSame('b2222c00-0000-0000-0000-000000862097', $response['body']['clientId']);
|
|
$this->assertSame('example.fusionauth.io', $response['body']['endpoint']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('fusionauth', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2FusionAuthPartialPreservesSecret(): void
|
|
{
|
|
// The `clientSecret` and `endpoint` live in the JSON blob and must
|
|
// survive when omitted on a subsequent call that only changes clientId.
|
|
$this->updateOAuth2('fusionauth', [
|
|
'clientId' => 'fusionauth-merge-client',
|
|
'clientSecret' => 'fusionauth-merge-secret',
|
|
'endpoint' => 'merge.fusionauth.io',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('fusionauth', [
|
|
'clientId' => 'fusionauth-rotated-client',
|
|
]);
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('fusionauth-rotated-client', $response['body']['clientId']);
|
|
$this->assertSame('merge.fusionauth.io', $response['body']['endpoint']);
|
|
|
|
// Confirm clientSecret survived the omitted-field merge by enabling
|
|
// without re-sending endpoint.
|
|
$enable = $this->updateOAuth2('fusionauth', [
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertTrue($enable['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('fusionauth', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2FusionAuthEnableAndReadBack(): void
|
|
{
|
|
$update = $this->updateOAuth2('fusionauth', [
|
|
'clientId' => 'fusionauth-enable-client',
|
|
'clientSecret' => 'fusionauth-enable-secret',
|
|
'endpoint' => 'enable.fusionauth.io',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide clientSecret while keeping clientId and endpoint.
|
|
$get = $this->getOAuth2Provider('fusionauth');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('fusionauth-enable-client', $get['body']['clientId']);
|
|
$this->assertSame('enable.fusionauth.io', $get['body']['endpoint']);
|
|
$this->assertSame('', $get['body']['clientSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('fusionauth', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Keycloak (clientId + clientSecret + optional endpoint + optional realmName)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2KeycloakAllowsOmittedEndpointWhenDisabled(): void
|
|
{
|
|
$response = $this->updateOAuth2('keycloak', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'realmName' => 'appwrite-realm',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('keycloak', $response['body']['$id']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('keycloak', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'realmName' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2KeycloakEmptyEndpointRejectedWhenEnabling(): void
|
|
{
|
|
$response = $this->updateOAuth2('keycloak', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'endpoint' => '',
|
|
'realmName' => 'appwrite-realm',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2KeycloakAllowsOmittedRealmNameWhenDisabled(): void
|
|
{
|
|
$response = $this->updateOAuth2('keycloak', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'endpoint' => 'keycloak.example.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('keycloak', $response['body']['$id']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('keycloak', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'realmName' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2KeycloakEmptyRealmNameRejectedWhenEnabling(): void
|
|
{
|
|
$response = $this->updateOAuth2('keycloak', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'endpoint' => 'keycloak.example.com',
|
|
'realmName' => '',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2Keycloak(): void
|
|
{
|
|
$response = $this->updateOAuth2('keycloak', [
|
|
'clientId' => 'appwrite-o0000000st-app',
|
|
'clientSecret' => 'keycloak-secret',
|
|
'endpoint' => 'keycloak.example.com',
|
|
'realmName' => 'appwrite-realm',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('keycloak', $response['body']['$id']);
|
|
$this->assertSame('appwrite-o0000000st-app', $response['body']['clientId']);
|
|
$this->assertSame('keycloak.example.com', $response['body']['endpoint']);
|
|
$this->assertSame('appwrite-realm', $response['body']['realmName']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('keycloak', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'realmName' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2KeycloakPartialPreservesSecret(): void
|
|
{
|
|
// The `clientSecret`, `endpoint`, and `realmName` live in the JSON
|
|
// blob and must survive when omitted on a subsequent call that only
|
|
// changes clientId.
|
|
$this->updateOAuth2('keycloak', [
|
|
'clientId' => 'keycloak-merge-client',
|
|
'clientSecret' => 'keycloak-merge-secret',
|
|
'endpoint' => 'merge.keycloak.com',
|
|
'realmName' => 'merge-realm',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('keycloak', [
|
|
'clientId' => 'keycloak-rotated-client',
|
|
]);
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('keycloak-rotated-client', $response['body']['clientId']);
|
|
$this->assertSame('merge.keycloak.com', $response['body']['endpoint']);
|
|
$this->assertSame('merge-realm', $response['body']['realmName']);
|
|
|
|
// Confirm clientSecret survived the omitted-field merge by enabling
|
|
// without re-sending endpoint or realmName.
|
|
$enable = $this->updateOAuth2('keycloak', [
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertTrue($enable['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('keycloak', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'realmName' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2KeycloakEnableAndReadBack(): void
|
|
{
|
|
$update = $this->updateOAuth2('keycloak', [
|
|
'clientId' => 'keycloak-enable-client',
|
|
'clientSecret' => 'keycloak-enable-secret',
|
|
'endpoint' => 'enable.keycloak.com',
|
|
'realmName' => 'enable-realm',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide clientSecret while keeping clientId, endpoint, realmName.
|
|
$get = $this->getOAuth2Provider('keycloak');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('keycloak-enable-client', $get['body']['clientId']);
|
|
$this->assertSame('enable.keycloak.com', $get['body']['endpoint']);
|
|
$this->assertSame('enable-realm', $get['body']['realmName']);
|
|
$this->assertSame('', $get['body']['clientSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('keycloak', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'endpoint' => '',
|
|
'realmName' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Microsoft (applicationId + applicationSecret + optional tenant)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2MicrosoftAllowsOmittedTenantWhenDisabled(): void
|
|
{
|
|
$response = $this->updateOAuth2('microsoft', [
|
|
'applicationId' => 'whatever',
|
|
'applicationSecret' => 'whatever',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('microsoft', $response['body']['$id']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('microsoft', [
|
|
'applicationId' => '',
|
|
'applicationSecret' => '',
|
|
'tenant' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2MicrosoftEmptyTenantRejectedWhenEnabling(): void
|
|
{
|
|
$response = $this->updateOAuth2('microsoft', [
|
|
'applicationId' => 'whatever',
|
|
'applicationSecret' => 'whatever',
|
|
'tenant' => '',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2Microsoft(): void
|
|
{
|
|
$response = $this->updateOAuth2('microsoft', [
|
|
'applicationId' => '00001111-aaaa-2222-bbbb-3333cccc4444',
|
|
'applicationSecret' => 'A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u',
|
|
'tenant' => 'common',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('microsoft', $response['body']['$id']);
|
|
$this->assertSame('00001111-aaaa-2222-bbbb-3333cccc4444', $response['body']['applicationId']);
|
|
$this->assertSame('common', $response['body']['tenant']);
|
|
// Custom param names: applicationId/applicationSecret, not clientId/clientSecret.
|
|
$this->assertArrayNotHasKey('clientId', $response['body']);
|
|
$this->assertArrayNotHasKey('clientSecret', $response['body']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('microsoft', [
|
|
'applicationId' => '',
|
|
'applicationSecret' => '',
|
|
'tenant' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2MicrosoftPartialPreservesSecret(): void
|
|
{
|
|
// Seed full credentials.
|
|
$this->updateOAuth2('microsoft', [
|
|
'applicationId' => 'seed-app-id',
|
|
'applicationSecret' => 'seed-app-secret',
|
|
'tenant' => 'common',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Patch with only a new applicationId, leaving applicationSecret and
|
|
// tenant omitted. The stored JSON values must not be wiped.
|
|
$response = $this->updateOAuth2('microsoft', [
|
|
'applicationId' => 'updated-app-id',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('updated-app-id', $response['body']['applicationId']);
|
|
$this->assertSame('common', $response['body']['tenant']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('microsoft', [
|
|
'applicationId' => '',
|
|
'applicationSecret' => '',
|
|
'tenant' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2MicrosoftEnableAndReadBack(): void
|
|
{
|
|
$update = $this->updateOAuth2('microsoft', [
|
|
'applicationId' => 'microsoft-enable-app',
|
|
'applicationSecret' => 'microsoft-enable-secret',
|
|
'tenant' => 'common',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide applicationSecret while keeping applicationId/tenant.
|
|
$get = $this->getOAuth2Provider('microsoft');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('microsoft-enable-app', $get['body']['applicationId']);
|
|
$this->assertSame('common', $get['body']['tenant']);
|
|
$this->assertSame('', $get['body']['applicationSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('microsoft', [
|
|
'applicationId' => '',
|
|
'applicationSecret' => '',
|
|
'tenant' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Gitlab (applicationId + secret + optional endpoint, custom names)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2Gitlab(): void
|
|
{
|
|
$response = $this->updateOAuth2('gitlab', [
|
|
'applicationId' => 'd41ffe0000000000000000000000000000000000000000000000000000d5e252',
|
|
'secret' => 'gloas-838cfa00',
|
|
'endpoint' => 'https://gitlab.example.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('gitlab', $response['body']['$id']);
|
|
$this->assertSame('d41ffe0000000000000000000000000000000000000000000000000000d5e252', $response['body']['applicationId']);
|
|
$this->assertSame('https://gitlab.example.com', $response['body']['endpoint']);
|
|
// Custom names — the response model exposes `applicationId`/`secret`.
|
|
$this->assertArrayNotHasKey('clientId', $response['body']);
|
|
$this->assertArrayNotHasKey('clientSecret', $response['body']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('gitlab', [
|
|
'applicationId' => '',
|
|
'secret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2GitlabInvalidEndpoint(): void
|
|
{
|
|
$response = $this->updateOAuth2('gitlab', [
|
|
'applicationId' => 'whatever',
|
|
'secret' => 'whatever',
|
|
'endpoint' => 'not a url',
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2GitlabPartialEndpoint(): void
|
|
{
|
|
$this->updateOAuth2('gitlab', [
|
|
'applicationId' => 'gitlab-seed-app',
|
|
'secret' => 'gitlab-seed-secret',
|
|
'endpoint' => 'https://seed.gitlab.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('gitlab', [
|
|
'endpoint' => 'https://updated.gitlab.com',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('https://updated.gitlab.com', $response['body']['endpoint']);
|
|
$this->assertSame('gitlab-seed-app', $response['body']['applicationId']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('gitlab', [
|
|
'applicationId' => '',
|
|
'secret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2GitlabPartialPreservesEachField(): void
|
|
{
|
|
// testUpdateOAuth2GitlabPartialEndpoint covers patching only `endpoint`.
|
|
// Cover patching `secret` alone (must not wipe applicationId/endpoint)
|
|
// and `applicationId` alone (must not wipe the JSON-blob endpoint).
|
|
$this->updateOAuth2('gitlab', [
|
|
'applicationId' => 'gitlab-merge-app',
|
|
'secret' => 'gitlab-merge-secret',
|
|
'endpoint' => 'https://merge.gitlab.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Patch only `secret`.
|
|
$secretOnly = $this->updateOAuth2('gitlab', [
|
|
'secret' => 'gitlab-rotated-secret',
|
|
]);
|
|
$this->assertSame(200, $secretOnly['headers']['status-code']);
|
|
$this->assertSame('gitlab-merge-app', $secretOnly['body']['applicationId']);
|
|
$this->assertSame('https://merge.gitlab.com', $secretOnly['body']['endpoint']);
|
|
|
|
// Patch only `applicationId`.
|
|
$idOnly = $this->updateOAuth2('gitlab', [
|
|
'applicationId' => 'gitlab-rotated-app',
|
|
]);
|
|
$this->assertSame(200, $idOnly['headers']['status-code']);
|
|
$this->assertSame('gitlab-rotated-app', $idOnly['body']['applicationId']);
|
|
$this->assertSame('https://merge.gitlab.com', $idOnly['body']['endpoint']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('gitlab', [
|
|
'applicationId' => '',
|
|
'secret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2GitlabEnableAndReadBack(): void
|
|
{
|
|
$update = $this->updateOAuth2('gitlab', [
|
|
'applicationId' => 'gitlab-enable-app',
|
|
'secret' => 'gitlab-enable-secret',
|
|
'endpoint' => 'https://enable.gitlab.com',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide `secret` while keeping applicationId and endpoint.
|
|
$get = $this->getOAuth2Provider('gitlab');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('gitlab-enable-app', $get['body']['applicationId']);
|
|
$this->assertSame('https://enable.gitlab.com', $get['body']['endpoint']);
|
|
$this->assertSame('', $get['body']['secret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('gitlab', [
|
|
'applicationId' => '',
|
|
'secret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2GitlabEndpointAcceptsEmpty(): void
|
|
{
|
|
// The `endpoint` validator is `Nullable(URL(allowEmpty: true))`. Passing
|
|
// `''` must clear the stored value rather than 400 on URL validation.
|
|
$this->updateOAuth2('gitlab', [
|
|
'applicationId' => 'gitlab-clear-app',
|
|
'secret' => 'gitlab-clear-secret',
|
|
'endpoint' => 'https://before.gitlab.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('gitlab', [
|
|
'endpoint' => '',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('', $response['body']['endpoint']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('gitlab', [
|
|
'applicationId' => '',
|
|
'secret' => '',
|
|
'endpoint' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update OIDC (clientId + secret + wellKnownURL or 3 discovery URLs)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2OidcWithWellKnown(): void
|
|
{
|
|
$response = $this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-client',
|
|
'clientSecret' => 'oidc-secret',
|
|
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('https://idp.example.com/.well-known/openid-configuration', $response['body']['wellKnownURL']);
|
|
$this->assertArrayHasKey('authorizationURL', $response['body']);
|
|
$this->assertArrayHasKey('tokenURL', $response['body']);
|
|
$this->assertArrayHasKey('userInfoURL', $response['body']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcWithDiscoveryURLs(): void
|
|
{
|
|
$response = $this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-discovery',
|
|
'clientSecret' => 'oidc-discovery-secret',
|
|
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
|
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
|
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('https://idp.example.com/oauth2/authorize', $response['body']['authorizationURL']);
|
|
$this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenURL']);
|
|
$this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoURL']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcEnableMissingURLs(): void
|
|
{
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-no-urls',
|
|
'clientSecret' => 'oidc-no-urls',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcEnablePartialDiscoveryFails(): void
|
|
{
|
|
// Only authorization+token, missing userInfo — must fail to enable.
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-partial',
|
|
'clientSecret' => 'oidc-partial-secret',
|
|
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
|
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcEnableSucceedsWithWellKnown(): void
|
|
{
|
|
$update = $this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-enable-client',
|
|
'clientSecret' => 'oidc-enable-secret',
|
|
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide clientSecret while keeping clientId and the URL.
|
|
$get = $this->getOAuth2Provider('oidc');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('oidc-enable-client', $get['body']['clientId']);
|
|
$this->assertSame('https://idp.example.com/.well-known/openid-configuration', $get['body']['wellKnownURL']);
|
|
$this->assertSame('', $get['body']['clientSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcEnableInSeparateRequestWithWellKnown(): void
|
|
{
|
|
// Configure URLs first with `enabled: false`. Then enable in a SECOND
|
|
// request that omits all URL fields. The merge-on-enable logic in
|
|
// Oidc::handle() must see the previously-stored wellKnownEndpoint and
|
|
// allow the toggle. This is the headline feature of the merge logic.
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-split-wk-client',
|
|
'clientSecret' => 'oidc-split-wk-secret',
|
|
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$enable = $this->updateOAuth2('oidc', [
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertTrue($enable['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcEnableAcrossRequestsWithDiscoveryURLs(): void
|
|
{
|
|
// Reset to clean state — earlier tests in this section may have left
|
|
// partial URL state when running in any order.
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Request 1: configure two of the three discovery URLs.
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-split-discovery',
|
|
'clientSecret' => 'oidc-split-discovery-secret',
|
|
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
|
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Request 2: send only the third URL plus enable=true. The merged
|
|
// state must include the two stored URLs + the new one to satisfy
|
|
// the all-three-discovery-URLs branch of the enable check.
|
|
$enable = $this->updateOAuth2('oidc', [
|
|
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertTrue($enable['body']['enabled']);
|
|
|
|
// Confirm all three URLs ended up persisted (merge wrote the new
|
|
// userInfoURL while preserving the previously stored two).
|
|
$get = $this->getOAuth2Provider('oidc');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertSame('https://idp.example.com/oauth2/authorize', $get['body']['authorizationURL']);
|
|
$this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenURL']);
|
|
$this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoURL']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcEnableFailsAfterClearingWellKnown(): void
|
|
{
|
|
// Seed wellKnownURL only (no discovery URLs).
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-clear-then-enable',
|
|
'clientSecret' => 'oidc-clear-then-enable-secret',
|
|
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Clear wellKnownURL and try to enable in the same request. Merge
|
|
// sees `wellKnown=''` (the cleared empty wins over the stored value
|
|
// because the new value is non-null) and no discovery URLs → 400.
|
|
// This is the inverse of testUpdateOAuth2OidcEnableInSeparateRequestWithWellKnown:
|
|
// confirms the merge correctly *replaces* with empty rather than
|
|
// falling back to the stored non-empty value.
|
|
$response = $this->updateOAuth2('oidc', [
|
|
'wellKnownURL' => '',
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcSwitchModesWellKnownToDiscovery(): void
|
|
{
|
|
// Configure with wellKnownURL, then switch to the three-discovery-URL
|
|
// mode in a single request: clear wellKnown, set the three URLs,
|
|
// enable. Merge sees wellKnown='' AND all three discovery URLs set →
|
|
// hasAllDiscovery branch passes.
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-switch-client',
|
|
'clientSecret' => 'oidc-switch-secret',
|
|
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$switch = $this->updateOAuth2('oidc', [
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
|
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
|
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(200, $switch['headers']['status-code']);
|
|
$this->assertTrue($switch['body']['enabled']);
|
|
$this->assertSame('', $switch['body']['wellKnownURL']);
|
|
$this->assertSame('https://idp.example.com/oauth2/authorize', $switch['body']['authorizationURL']);
|
|
$this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenURL']);
|
|
$this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoURL']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcURLsAcceptEmpty(): void
|
|
{
|
|
// All four URL fields use `Nullable(URL(allowEmpty: true))`. Passing `''`
|
|
// for each must clear them rather than 400 on URL validation.
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => 'oidc-clear-client',
|
|
'clientSecret' => 'oidc-clear-secret',
|
|
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
|
|
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
|
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
|
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('oidc', [
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('', $response['body']['wellKnownURL']);
|
|
$this->assertSame('', $response['body']['authorizationURL']);
|
|
$this->assertSame('', $response['body']['tokenURL']);
|
|
$this->assertSame('', $response['body']['userInfoURL']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OidcBackwardCompatibleResponseFormat(): void
|
|
{
|
|
// Reset to clean state
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'wellKnownURL' => '',
|
|
'authorizationURL' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$headers = [
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
'x-appwrite-response-format' => '1.9.3',
|
|
];
|
|
$headers = \array_merge($headers, $this->getHeaders());
|
|
|
|
// Update using OLD param names (aliases must still work)
|
|
$response = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/project/oauth2/oidc',
|
|
$headers,
|
|
[
|
|
'clientId' => 'oidc-compat-client',
|
|
'clientSecret' => 'oidc-compat-secret',
|
|
'tokenUrl' => 'https://idp.example.com/oauth2/token',
|
|
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
|
|
'enabled' => false,
|
|
],
|
|
);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertArrayHasKey('tokenUrl', $response['body']);
|
|
$this->assertArrayHasKey('userInfoUrl', $response['body']);
|
|
$this->assertArrayNotHasKey('tokenURL', $response['body']);
|
|
$this->assertArrayNotHasKey('userInfoURL', $response['body']);
|
|
$this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']);
|
|
$this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']);
|
|
|
|
// GET with 1.9.3 format must also return old param names
|
|
$get = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/project/oauth2/oidc',
|
|
$headers,
|
|
);
|
|
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertArrayHasKey('tokenUrl', $get['body']);
|
|
$this->assertArrayHasKey('userInfoUrl', $get['body']);
|
|
$this->assertArrayNotHasKey('tokenURL', $get['body']);
|
|
$this->assertArrayNotHasKey('userInfoURL', $get['body']);
|
|
$this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']);
|
|
$this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']);
|
|
|
|
// LIST with 1.9.3 format must also return old param names for OIDC
|
|
$list = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/project/oauth2',
|
|
$headers,
|
|
);
|
|
|
|
$this->assertSame(200, $list['headers']['status-code']);
|
|
$oidcEntry = null;
|
|
foreach ($list['body']['providers'] as $provider) {
|
|
if ($provider['$id'] === 'oidc') {
|
|
$oidcEntry = $provider;
|
|
break;
|
|
}
|
|
}
|
|
$this->assertNotNull($oidcEntry, 'OIDC provider missing from listOAuth2Providers response');
|
|
$this->assertArrayHasKey('tokenUrl', $oidcEntry);
|
|
$this->assertArrayHasKey('userInfoUrl', $oidcEntry);
|
|
$this->assertArrayNotHasKey('tokenURL', $oidcEntry);
|
|
$this->assertArrayNotHasKey('userInfoURL', $oidcEntry);
|
|
$this->assertSame('https://idp.example.com/oauth2/token', $oidcEntry['tokenUrl']);
|
|
$this->assertSame('https://idp.example.com/oauth2/userinfo', $oidcEntry['userInfoUrl']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('oidc', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'tokenURL' => '',
|
|
'userInfoURL' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Okta (clientId + clientSecret + optional domain/authServer)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2Okta(): void
|
|
{
|
|
$response = $this->updateOAuth2('okta', [
|
|
'clientId' => '0oa00000000000000698',
|
|
'clientSecret' => 'okta-secret',
|
|
'domain' => 'trial-6400025.okta.com',
|
|
'authorizationServerId' => 'aus000000000000000h7z',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('okta', $response['body']['$id']);
|
|
$this->assertSame('0oa00000000000000698', $response['body']['clientId']);
|
|
$this->assertSame('trial-6400025.okta.com', $response['body']['domain']);
|
|
$this->assertSame('aus000000000000000h7z', $response['body']['authorizationServerId']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'domain' => '',
|
|
'authorizationServerId' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OktaInvalidDomain(): void
|
|
{
|
|
$response = $this->updateOAuth2('okta', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'domain' => 'https://trial-6400025.okta.com/',
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2OktaEnableRequiresDomain(): void
|
|
{
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'domain' => '',
|
|
'authorizationServerId' => '',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('okta', [
|
|
'clientId' => 'okta-no-domain',
|
|
'clientSecret' => 'okta-no-domain-secret',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OktaEnableSucceedsWithDomain(): void
|
|
{
|
|
$update = $this->updateOAuth2('okta', [
|
|
'clientId' => 'okta-enable-client',
|
|
'clientSecret' => 'okta-enable-secret',
|
|
'domain' => 'enable.okta.com',
|
|
'authorizationServerId' => 'aus000000000000000h7z',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide clientSecret while keeping clientId, domain and authServerId.
|
|
$get = $this->getOAuth2Provider('okta');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('okta-enable-client', $get['body']['clientId']);
|
|
$this->assertSame('enable.okta.com', $get['body']['domain']);
|
|
$this->assertSame('aus000000000000000h7z', $get['body']['authorizationServerId']);
|
|
$this->assertSame('', $get['body']['clientSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'domain' => '',
|
|
'authorizationServerId' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OktaPartialPreservesEachField(): void
|
|
{
|
|
// Okta has no field-by-field partial test in the existing suite. Cover
|
|
// each of `domain`, `authorizationServerId`, and `clientSecret` being
|
|
// patched alone — all three live in the same JSON blob.
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => 'okta-merge-client',
|
|
'clientSecret' => 'okta-merge-secret',
|
|
'domain' => 'merge.okta.com',
|
|
'authorizationServerId' => 'aus000000000000merge',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Patch only `domain` — others must survive.
|
|
$domainOnly = $this->updateOAuth2('okta', [
|
|
'domain' => 'rotated.okta.com',
|
|
]);
|
|
$this->assertSame(200, $domainOnly['headers']['status-code']);
|
|
$this->assertSame('rotated.okta.com', $domainOnly['body']['domain']);
|
|
$this->assertSame('okta-merge-client', $domainOnly['body']['clientId']);
|
|
$this->assertSame('aus000000000000merge', $domainOnly['body']['authorizationServerId']);
|
|
|
|
// Patch only `authorizationServerId`.
|
|
$authServerOnly = $this->updateOAuth2('okta', [
|
|
'authorizationServerId' => 'aus000000000rotated00',
|
|
]);
|
|
$this->assertSame(200, $authServerOnly['headers']['status-code']);
|
|
$this->assertSame('rotated.okta.com', $authServerOnly['body']['domain']);
|
|
$this->assertSame('aus000000000rotated00', $authServerOnly['body']['authorizationServerId']);
|
|
|
|
// Patch only `clientSecret` — domain and authServerId in the JSON blob
|
|
// must survive. Confirm the rotated secret persisted by enabling.
|
|
$secretOnly = $this->updateOAuth2('okta', [
|
|
'clientSecret' => 'okta-rotated-secret',
|
|
]);
|
|
$this->assertSame(200, $secretOnly['headers']['status-code']);
|
|
$this->assertSame('rotated.okta.com', $secretOnly['body']['domain']);
|
|
$this->assertSame('aus000000000rotated00', $secretOnly['body']['authorizationServerId']);
|
|
|
|
$enable = $this->updateOAuth2('okta', ['enabled' => true]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertTrue($enable['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'domain' => '',
|
|
'authorizationServerId' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OktaAuthServerIdAcceptsEmpty(): void
|
|
{
|
|
// `authorizationServerId` is `Nullable(Text(256, 0))`. Passing `''`
|
|
// must clear the stored value while leaving the rest of the JSON blob
|
|
// (clientSecret, oktaDomain) untouched.
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => 'okta-clear-auth-server',
|
|
'clientSecret' => 'okta-clear-auth-server-secret',
|
|
'domain' => 'authserver.okta.com',
|
|
'authorizationServerId' => 'aus0000000000beforeauth',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('okta', [
|
|
'authorizationServerId' => '',
|
|
]);
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('', $response['body']['authorizationServerId']);
|
|
// domain (also stored in the JSON blob) must NOT have been wiped.
|
|
$this->assertSame('authserver.okta.com', $response['body']['domain']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'domain' => '',
|
|
'authorizationServerId' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2OktaDomainAcceptsEmpty(): void
|
|
{
|
|
// The `domain` validator is `Nullable(Domain(allowEmpty: true))`. Passing
|
|
// `''` must clear the stored value rather than 400 on Domain validation.
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => 'okta-clear-client',
|
|
'clientSecret' => 'okta-clear-secret',
|
|
'domain' => 'before.okta.com',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('okta', [
|
|
'domain' => '',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('', $response['body']['domain']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('okta', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'domain' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Dropbox (custom param names: appKey + appSecret)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2DropboxFieldNames(): void
|
|
{
|
|
$response = $this->updateOAuth2('dropbox', [
|
|
'appKey' => 'jl000000000009t',
|
|
'appSecret' => 'g200000000000vw',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('dropbox', $response['body']['$id']);
|
|
$this->assertSame('jl000000000009t', $response['body']['appKey']);
|
|
$this->assertArrayHasKey('appSecret', $response['body']);
|
|
$this->assertArrayNotHasKey('clientId', $response['body']);
|
|
$this->assertArrayNotHasKey('clientSecret', $response['body']);
|
|
|
|
// GET enforces write-only on the secret regardless of the custom name.
|
|
$get = $this->getOAuth2Provider('dropbox');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertSame('jl000000000009t', $get['body']['appKey']);
|
|
$this->assertSame('', $get['body']['appSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('dropbox', [
|
|
'appKey' => '',
|
|
'appSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2DropboxPartial(): void
|
|
{
|
|
// Seed both fields, then patch only `appKey` and verify `appSecret`
|
|
// persists by enabling — Dropbox has no verifyCredentials() hook, so
|
|
// enabling succeeds purely from local state.
|
|
$this->updateOAuth2('dropbox', [
|
|
'appKey' => 'dropbox-seed-key',
|
|
'appSecret' => 'dropbox-seed-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$response = $this->updateOAuth2('dropbox', [
|
|
'appKey' => 'dropbox-updated-key',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('dropbox-updated-key', $response['body']['appKey']);
|
|
|
|
$enable = $this->updateOAuth2('dropbox', [
|
|
'enabled' => true,
|
|
]);
|
|
$this->assertSame(200, $enable['headers']['status-code']);
|
|
$this->assertSame(true, $enable['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('dropbox', [
|
|
'appKey' => '',
|
|
'appSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2DropboxEnableAndReadBack(): void
|
|
{
|
|
$update = $this->updateOAuth2('dropbox', [
|
|
'appKey' => 'dropbox-enable-key',
|
|
'appSecret' => 'dropbox-enable-secret',
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide `appSecret` while keeping `appKey`.
|
|
$get = $this->getOAuth2Provider('dropbox');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('dropbox-enable-key', $get['body']['appKey']);
|
|
$this->assertSame('', $get['body']['appSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('dropbox', [
|
|
'appKey' => '',
|
|
'appSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Paypal Sandbox (inherits from Paypal — independent provider ID)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2PaypalSandbox(): void
|
|
{
|
|
$response = $this->updateOAuth2('paypalSandbox', [
|
|
'clientId' => 'paypal-sandbox-client',
|
|
'clientSecret' => 'paypal-sandbox-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('paypalSandbox', $response['body']['$id']);
|
|
$this->assertSame('paypal-sandbox-client', $response['body']['clientId']);
|
|
|
|
// Sandbox is independent of the regular paypal entry.
|
|
$regular = $this->getOAuth2Provider('paypal');
|
|
$this->assertSame(200, $regular['headers']['status-code']);
|
|
$this->assertSame('paypal', $regular['body']['$id']);
|
|
$this->assertNotSame('paypal-sandbox-client', $regular['body']['clientId']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('paypalSandbox', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2PaypalSandboxResponseModel(): void
|
|
{
|
|
// PaypalSandbox inherits from Paypal: param/response field is
|
|
// `secretKey` instead of `clientSecret`. A regression that adds the
|
|
// default `clientSecret` to the response model would leak the
|
|
// unwritten field; pin its absence on both PATCH and GET.
|
|
$update = $this->updateOAuth2('paypalSandbox', [
|
|
'clientId' => 'paypal-sandbox-shape',
|
|
'secretKey' => 'paypal-sandbox-shape-secret',
|
|
'enabled' => false,
|
|
]);
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertArrayHasKey('secretKey', $update['body']);
|
|
$this->assertArrayNotHasKey('clientSecret', $update['body']);
|
|
|
|
$get = $this->getOAuth2Provider('paypalSandbox');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertArrayHasKey('secretKey', $get['body']);
|
|
$this->assertArrayNotHasKey('clientSecret', $get['body']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('paypalSandbox', [
|
|
'clientId' => '',
|
|
'secretKey' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2PaypalDoesNotAffectSandbox(): void
|
|
{
|
|
// Reverse direction: writing to regular paypal must leave sandbox state intact.
|
|
$this->updateOAuth2('paypalSandbox', [
|
|
'clientId' => 'sandbox-untouched',
|
|
'clientSecret' => 'sandbox-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->updateOAuth2('paypal', [
|
|
'clientId' => 'paypal-prod',
|
|
'secretKey' => 'paypal-prod-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$sandbox = $this->getOAuth2Provider('paypalSandbox');
|
|
$this->assertSame(200, $sandbox['headers']['status-code']);
|
|
$this->assertSame('sandbox-untouched', $sandbox['body']['clientId']);
|
|
|
|
// Cleanup both
|
|
$this->updateOAuth2('paypal', [
|
|
'clientId' => '',
|
|
'secretKey' => '',
|
|
'enabled' => false,
|
|
]);
|
|
$this->updateOAuth2('paypalSandbox', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Tradeshift Sandbox (inherits from Tradeshift — independent provider ID)
|
|
// =========================================================================
|
|
|
|
public function testUpdateOAuth2TradeshiftBox(): void
|
|
{
|
|
$response = $this->updateOAuth2('tradeshiftBox', [
|
|
'oauth2ClientId' => 'tradeshift-sandbox-client',
|
|
'oauth2ClientSecret' => 'tradeshift-sandbox-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('tradeshiftBox', $response['body']['$id']);
|
|
$this->assertSame('tradeshift-sandbox-client', $response['body']['oauth2ClientId']);
|
|
|
|
// Sandbox is independent of the regular tradeshift entry.
|
|
$regular = $this->getOAuth2Provider('tradeshift');
|
|
$this->assertSame(200, $regular['headers']['status-code']);
|
|
$this->assertSame('tradeshift', $regular['body']['$id']);
|
|
$this->assertNotSame('tradeshift-sandbox-client', $regular['body']['oauth2ClientId']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('tradeshiftBox', [
|
|
'oauth2ClientId' => '',
|
|
'oauth2ClientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2TradeshiftBoxResponseModel(): void
|
|
{
|
|
// TradeshiftSandbox inherits from Tradeshift: both clientId AND
|
|
// clientSecret are renamed (oauth2ClientId / oauth2ClientSecret).
|
|
// Pin that the default field names are absent from PATCH and GET
|
|
// responses so a stray addition to the response model is caught.
|
|
$update = $this->updateOAuth2('tradeshiftBox', [
|
|
'oauth2ClientId' => 'tradeshift-box-shape',
|
|
'oauth2ClientSecret' => 'tradeshift-box-shape-secret',
|
|
'enabled' => false,
|
|
]);
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertArrayHasKey('oauth2ClientId', $update['body']);
|
|
$this->assertArrayHasKey('oauth2ClientSecret', $update['body']);
|
|
$this->assertArrayNotHasKey('clientId', $update['body']);
|
|
$this->assertArrayNotHasKey('clientSecret', $update['body']);
|
|
|
|
$get = $this->getOAuth2Provider('tradeshiftBox');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertArrayHasKey('oauth2ClientId', $get['body']);
|
|
$this->assertArrayHasKey('oauth2ClientSecret', $get['body']);
|
|
$this->assertArrayNotHasKey('clientId', $get['body']);
|
|
$this->assertArrayNotHasKey('clientSecret', $get['body']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('tradeshiftBox', [
|
|
'oauth2ClientId' => '',
|
|
'oauth2ClientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2TradeshiftDoesNotAffectSandbox(): void
|
|
{
|
|
// Reverse direction: writing to regular tradeshift must not touch sandbox state.
|
|
$this->updateOAuth2('tradeshiftBox', [
|
|
'oauth2ClientId' => 'tradeshift-sandbox-untouched',
|
|
'oauth2ClientSecret' => 'tradeshift-sandbox-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->updateOAuth2('tradeshift', [
|
|
'oauth2ClientId' => 'tradeshift-prod',
|
|
'oauth2ClientSecret' => 'tradeshift-prod-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$sandbox = $this->getOAuth2Provider('tradeshiftBox');
|
|
$this->assertSame(200, $sandbox['headers']['status-code']);
|
|
$this->assertSame('tradeshift-sandbox-untouched', $sandbox['body']['oauth2ClientId']);
|
|
|
|
// Cleanup both
|
|
$this->updateOAuth2('tradeshift', [
|
|
'oauth2ClientId' => '',
|
|
'oauth2ClientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
$this->updateOAuth2('tradeshiftBox', [
|
|
'oauth2ClientId' => '',
|
|
'oauth2ClientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update Google (clientId + clientSecret + optional prompt)
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Default prompt MUST run before any other Google test that sets a custom
|
|
* prompt value. The global resetProjectOAuth2() only clears Amazon state,
|
|
* so Google state leaks across tests in the same class. Running this first
|
|
* guarantees the stored JSON blob has no pre-existing "prompt" key.
|
|
*/
|
|
public function testUpdateOAuth2GoogleDefaultPrompt(): void
|
|
{
|
|
// When prompt is omitted and nothing is stored, the default is ['consent'].
|
|
$response = $this->updateOAuth2('google', [
|
|
'clientId' => 'google-default-client',
|
|
'clientSecret' => 'google-default-secret',
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame(['consent'], $response['body']['prompt']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('google', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2Google(): void
|
|
{
|
|
$response = $this->updateOAuth2('google', [
|
|
'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
|
|
'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
|
|
'prompt' => ['select_account'],
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('google', $response['body']['$id']);
|
|
$this->assertSame('120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com', $response['body']['clientId']);
|
|
$this->assertSame(['select_account'], $response['body']['prompt']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('google', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2GooglePartialPreservesPrompt(): void
|
|
{
|
|
// Seed clientSecret + prompt.
|
|
$this->updateOAuth2('google', [
|
|
'clientId' => 'google-seed-client',
|
|
'clientSecret' => 'google-seed-secret',
|
|
'prompt' => ['consent', 'select_account'],
|
|
'enabled' => false,
|
|
]);
|
|
|
|
// Update only clientId.
|
|
$response = $this->updateOAuth2('google', [
|
|
'clientId' => 'google-rotated-client',
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame('google-rotated-client', $response['body']['clientId']);
|
|
$this->assertSame(['consent', 'select_account'], $response['body']['prompt']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('google', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2GooglePromptNoneAloneRejected(): void
|
|
{
|
|
$response = $this->updateOAuth2('google', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'prompt' => ['none', 'consent'],
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2GooglePromptEmptyArrayRejected(): void
|
|
{
|
|
$response = $this->updateOAuth2('google', [
|
|
'clientId' => 'whatever',
|
|
'clientSecret' => 'whatever',
|
|
'prompt' => [],
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(400, $response['headers']['status-code']);
|
|
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
|
}
|
|
|
|
public function testUpdateOAuth2GooglePromptNoneAloneAccepted(): void
|
|
{
|
|
$response = $this->updateOAuth2('google', [
|
|
'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
|
|
'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
|
|
'prompt' => ['none'],
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
$this->assertSame(['none'], $response['body']['prompt']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('google', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
public function testUpdateOAuth2GoogleEnableAndReadBack(): void
|
|
{
|
|
$update = $this->updateOAuth2('google', [
|
|
'clientId' => 'google-enable-client',
|
|
'clientSecret' => 'google-enable-secret',
|
|
'prompt' => ['select_account'],
|
|
'enabled' => true,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertTrue($update['body']['enabled']);
|
|
|
|
// GET must hide clientSecret while keeping clientId and prompt.
|
|
$get = $this->getOAuth2Provider('google');
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertTrue($get['body']['enabled']);
|
|
$this->assertSame('google-enable-client', $get['body']['clientId']);
|
|
$this->assertSame(['select_account'], $get['body']['prompt']);
|
|
$this->assertSame('', $get['body']['clientSecret']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2('google', [
|
|
'clientId' => '',
|
|
'clientSecret' => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Smoke test: every plain (clientId + clientSecret) provider
|
|
//
|
|
// Ensures each provider's Update endpoint is wired up correctly: routing,
|
|
// provider class, response model and `$id`. Custom-shaped providers
|
|
// (apple, auth0, authentik, fusionauth, gitlab, keycloak, microsoft, oidc,
|
|
// okta, dropbox) and sandboxes (paypalSandbox, tradeshiftSandbox) have
|
|
// dedicated tests above.
|
|
// Github is excluded because its `verifyCredentials()` hook is exercised
|
|
// separately.
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Provider, ID-field, secret-field. Many providers rename one or both of
|
|
* the two credential params (`clientId`/`clientSecret`) to match the
|
|
* upstream provider's terminology, so the smoke test parameterises both.
|
|
*
|
|
* @return array<string, array<string>>
|
|
*/
|
|
public static function plainProviders(): array
|
|
{
|
|
return [
|
|
'discord' => ['discord', 'clientId', 'clientSecret'],
|
|
'figma' => ['figma', 'clientId', 'clientSecret'],
|
|
'dailymotion' => ['dailymotion', 'apiKey', 'apiSecret'],
|
|
'bitbucket' => ['bitbucket', 'key', 'secret'],
|
|
'bitly' => ['bitly', 'clientId', 'clientSecret'],
|
|
'box' => ['box', 'clientId', 'clientSecret'],
|
|
'autodesk' => ['autodesk', 'clientId', 'clientSecret'],
|
|
'google' => ['google', 'clientId', 'clientSecret'],
|
|
'zoom' => ['zoom', 'clientId', 'clientSecret'],
|
|
'zoho' => ['zoho', 'clientId', 'clientSecret'],
|
|
'yandex' => ['yandex', 'clientId', 'clientSecret'],
|
|
'x' => ['x', 'customerKey', 'secretKey'],
|
|
'wordpress' => ['wordpress', 'clientId', 'clientSecret'],
|
|
'twitch' => ['twitch', 'clientId', 'clientSecret'],
|
|
'stripe' => ['stripe', 'clientId', 'apiSecretKey'],
|
|
'spotify' => ['spotify', 'clientId', 'clientSecret'],
|
|
'slack' => ['slack', 'clientId', 'clientSecret'],
|
|
'podio' => ['podio', 'clientId', 'clientSecret'],
|
|
'notion' => ['notion', 'oauthClientId', 'oauthClientSecret'],
|
|
'salesforce' => ['salesforce', 'customerKey', 'customerSecret'],
|
|
'yahoo' => ['yahoo', 'clientId', 'clientSecret'],
|
|
'linkedin' => ['linkedin', 'clientId', 'primaryClientSecret'],
|
|
'disqus' => ['disqus', 'publicKey', 'secretKey'],
|
|
'etsy' => ['etsy', 'keyString', 'sharedSecret'],
|
|
'facebook' => ['facebook', 'appId', 'appSecret'],
|
|
'tradeshift' => ['tradeshift', 'oauth2ClientId', 'oauth2ClientSecret'],
|
|
'paypal' => ['paypal', 'clientId', 'secretKey'],
|
|
'kick' => ['kick', 'clientId', 'clientSecret'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('plainProviders')]
|
|
public function testUpdateOAuth2PlainProvider(string $providerId, string $idField, string $secretField): void
|
|
{
|
|
$clientId = $providerId . '-smoke-client';
|
|
$clientSecret = $providerId . '-smoke-secret';
|
|
|
|
$update = $this->updateOAuth2($providerId, [
|
|
$idField => $clientId,
|
|
$secretField => $clientSecret,
|
|
'enabled' => false,
|
|
]);
|
|
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
$this->assertSame($providerId, $update['body']['$id']);
|
|
$this->assertSame($clientId, $update['body'][$idField]);
|
|
$this->assertFalse($update['body']['enabled']);
|
|
|
|
// GET round-trip — confirms the value actually persisted (catches a
|
|
// PATCH that only echoes input without writing) and that the secret
|
|
// is hidden on read.
|
|
$get = $this->getOAuth2Provider($providerId);
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
$this->assertSame($providerId, $get['body']['$id']);
|
|
$this->assertSame($clientId, $get['body'][$idField]);
|
|
$this->assertSame('', $get['body'][$secretField]);
|
|
$this->assertFalse($get['body']['enabled']);
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2($providerId, [
|
|
$idField => '',
|
|
$secretField => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* For providers that rename `clientId` / `clientSecret` to a custom field
|
|
* (e.g. `apiKey`/`apiSecret`, `customerKey`/`secretKey`, `oauthClientId`),
|
|
* the renamed field replaces the default — the response model must NOT
|
|
* also expose the default name. Catches a regression where adding a
|
|
* custom param name forgets to remove the default from the response.
|
|
*/
|
|
#[DataProvider('plainProviders')]
|
|
public function testUpdateOAuth2PlainProviderResponseDoesNotLeakDefaultNames(string $providerId, string $idField, string $secretField): void
|
|
{
|
|
if ($idField === 'clientId' && $secretField === 'clientSecret') {
|
|
// Default-named provider — nothing to leak. Avoids a no-op assertion.
|
|
$this->markTestSkipped("{$providerId} uses default field names.");
|
|
}
|
|
|
|
$update = $this->updateOAuth2($providerId, [
|
|
$idField => $providerId . '-leak-check-id',
|
|
$secretField => $providerId . '-leak-check-secret',
|
|
'enabled' => false,
|
|
]);
|
|
$this->assertSame(200, $update['headers']['status-code']);
|
|
|
|
if ($idField !== 'clientId') {
|
|
$this->assertArrayNotHasKey('clientId', $update['body'], "PATCH response for {$providerId} leaks default `clientId` despite using `{$idField}`.");
|
|
}
|
|
if ($secretField !== 'clientSecret') {
|
|
$this->assertArrayNotHasKey('clientSecret', $update['body'], "PATCH response for {$providerId} leaks default `clientSecret` despite using `{$secretField}`.");
|
|
}
|
|
|
|
$get = $this->getOAuth2Provider($providerId);
|
|
$this->assertSame(200, $get['headers']['status-code']);
|
|
if ($idField !== 'clientId') {
|
|
$this->assertArrayNotHasKey('clientId', $get['body'], "GET response for {$providerId} leaks default `clientId` despite using `{$idField}`.");
|
|
}
|
|
if ($secretField !== 'clientSecret') {
|
|
$this->assertArrayNotHasKey('clientSecret', $get['body'], "GET response for {$providerId} leaks default `clientSecret` despite using `{$secretField}`.");
|
|
}
|
|
|
|
// Cleanup
|
|
$this->updateOAuth2($providerId, [
|
|
$idField => '',
|
|
$secretField => '',
|
|
'enabled' => false,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Helpers
|
|
// =========================================================================
|
|
|
|
/**
|
|
* @param array<string, mixed> $params
|
|
*/
|
|
protected function updateOAuth2(string $provider, array $params, bool $authenticated = true): mixed
|
|
{
|
|
$headers = [
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
];
|
|
|
|
if ($authenticated) {
|
|
$headers = \array_merge($headers, $this->getHeaders());
|
|
}
|
|
|
|
return $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/project/oauth2/' . $provider,
|
|
$headers,
|
|
$params,
|
|
);
|
|
}
|
|
|
|
protected function getOAuth2Provider(string $providerId, bool $authenticated = true): mixed
|
|
{
|
|
$headers = [
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
];
|
|
|
|
if ($authenticated) {
|
|
$headers = \array_merge($headers, $this->getHeaders());
|
|
}
|
|
|
|
return $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/project/oauth2/' . $providerId,
|
|
$headers,
|
|
);
|
|
}
|
|
|
|
protected function listOAuth2Providers(?array $queries = null, ?bool $total = null, bool $authenticated = true): mixed
|
|
{
|
|
$params = [];
|
|
|
|
if ($queries !== null) {
|
|
$params['queries'] = $queries;
|
|
}
|
|
|
|
if ($total !== null) {
|
|
$params['total'] = $total;
|
|
}
|
|
|
|
$headers = [
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
];
|
|
|
|
if ($authenticated) {
|
|
$headers = \array_merge($headers, $this->getHeaders());
|
|
}
|
|
|
|
return $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/project/oauth2',
|
|
$headers,
|
|
$params,
|
|
);
|
|
}
|
|
}
|