Skip to content

test: add integration tests for token storage #7500

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ jobs:
env:
INTEGRATION_TEST: true
DEV_FEATURES_ENABLED: false
# Key encryption key (KEK) for the secret vault. (For integration tests use only)
SECRET_VAULT_KEK: DtPWS09unRXGuRScB60qXqCSsrjd22dUlXt/0oZgxSo=
DB_URL: postgres://postgres:postgres@localhost:5432/postgres

steps:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
env:
INTEGRATION_TEST: true
DEV_FEATURES_ENABLED: ${{ matrix.dev-features-enabled }}
# Key encryption key (KEK) for the secret vault. (For integration tests use only)
SECRET_VAULT_KEK: DtPWS09unRXGuRScB60qXqCSsrjd22dUlXt/0oZgxSo=
DB_URL: postgres://postgres:postgres@localhost:5432/postgres

steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export const defaultMetadata: ConnectorMetadata = {
'tr-TR': 'Social mock connector description',
},
readme: './README.md',
isTokenStorageSupported: true,
configTemplate: './docs/config-template.json',
};
66 changes: 51 additions & 15 deletions packages/connectors/connector-mock-social/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { z } from 'zod';
import type {
CreateConnector,
GetAuthorizationUri,
GetSession,
GetTokenResponseAndUserInfo,
GetUserInfo,
SocialConnector,
} from '@logto/connector-kit';
Expand All @@ -12,6 +14,7 @@ import {
ConnectorErrorCodes,
ConnectorType,
jsonGuard,
tokenResponseGuard,
} from '@logto/connector-kit';

import { defaultMetadata } from './constant.js';
Expand All @@ -33,21 +36,7 @@ const getAuthorizationUri: GetAuthorizationUri = async (
return `http://mock-social/?state=${state}&redirect_uri=${redirectUri}`;
};

const getUserInfo: GetUserInfo = async (data, getSession) => {
const dataGuard = z.object({
code: z.string(),
userId: z.optional(z.string()),
email: z.string().optional(),
phone: z.string().optional(),
name: z.string().optional(),
avatar: z.string().optional(),
});
const result = dataGuard.safeParse(data);

if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(data));
}

const validateConnectorSession = async (getSession: GetSession) => {
try {
const connectorSession = await getSession();
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Expand All @@ -60,6 +49,25 @@ const getUserInfo: GetUserInfo = async (data, getSession) => {
throw error;
}
}
};

const mockSocialDataGuard = z.object({
code: z.string(),
userId: z.optional(z.string()),
email: z.string().optional(),
phone: z.string().optional(),
name: z.string().optional(),
avatar: z.string().optional(),
});

const getUserInfo: GetUserInfo = async (data, getSession) => {
const result = mockSocialDataGuard.safeParse(data);

if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(data));
}

await validateConnectorSession(getSession);

const { code, userId, ...rest } = result.data;

Expand All @@ -71,13 +79,41 @@ const getUserInfo: GetUserInfo = async (data, getSession) => {
};
};

const getTokenResponseAndUserInfo: GetTokenResponseAndUserInfo = async (data, getSession) => {
const result = mockSocialDataGuard
.extend({
// Extend the data with the token response
tokenResponse: tokenResponseGuard.optional(),
})
.safeParse(data);

if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(data));
}

await validateConnectorSession(getSession);

const { code, userId, tokenResponse, ...rest } = result.data;

// For mock use only. Use to track the created user entity
return {
userInfo: {
id: userId ?? `mock-social-sub-${randomUUID()}`,
...rest,
rawData: jsonGuard.parse(data),
},
tokenResponse,
};
};

const createMockSocialConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
return {
metadata: defaultMetadata,
type: ConnectorType.Social,
configGuard: mockSocialConfigGuard,
getAuthorizationUri,
getUserInfo,
getTokenResponseAndUserInfo,
};
};

Expand Down
6 changes: 6 additions & 0 deletions packages/integration-tests/src/api/admin-user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
CreatePersonalAccessToken,
DesensitizedSocialTokenSetSecret,
Identities,
Identity,
MfaFactor,
Expand Down Expand Up @@ -116,6 +117,11 @@ export const putUserIdentity = async (userId: string, target: string, identity:
export const verifyUserPassword = async (userId: string, password: string) =>
authedAdminApi.post(`users/${userId}/password/verify`, { json: { password } });

export const getUserIdentityTokenSetRecord = async (userId: string, target: string) =>
authedAdminApi
.get(`users/${userId}/identities/${target}/secret`)
.json<DesensitizedSocialTokenSetSecret>();

export const getUserMfaVerifications = async (userId: string) =>
authedAdminApi.get(`users/${userId}/mfa-verifications`).json<MfaVerification[]>();

Expand Down
10 changes: 7 additions & 3 deletions packages/integration-tests/src/api/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@

export const updateConnectorConfig = async (
id: string,
config: Record<string, unknown>,
metadata?: Record<string, unknown>
body: {
config?: Record<string, unknown>;
metadata?: Record<string, unknown>;
enableTokenStorage?: boolean;
syncProfile?: boolean;
}
) =>
authedAdminApi
.patch(`connectors/${id}`, {
json: { config, metadata },
json: body,
})
.json<ConnectorResponse>();

Expand All @@ -73,7 +77,7 @@
receiver: string,
config: Record<string, unknown>,
locale?: string
) =>

Check warning on line 80 in packages/integration-tests/src/api/connector.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/integration-tests/src/api/connector.ts#L80

[max-params] Async arrow function has too many parameters (5). Maximum allowed is 4.
authedAdminApi.post(`connectors/${connectorFactoryId}/test`, {
json: { [receiverType]: receiver, config, locale },
});
Expand Down
8 changes: 5 additions & 3 deletions packages/integration-tests/src/helpers/experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @fileoverview This file contains the successful interaction flow helper functions that use the experience APIs.
*/

import { type SocialUserInfo } from '@logto/connector-kit';
import { type TokenResponse, type SocialUserInfo } from '@logto/connector-kit';
import {
InteractionEvent,
SignInIdentifier,
Expand Down Expand Up @@ -162,12 +162,14 @@ export const identifyUserWithEmailVerificationCode = async (

/**
*
* @param socialUserInfo The social user info that will be returned by the social connector.
* @param socialUserInfo The social user info and token response that will be returned by the social connector.
* @param registerNewUser Optional. If true, the user will be registered if the user does not exist, otherwise a error will be thrown if the user does not exist.
*/
export const signInWithSocial = async (
connectorId: string,
socialUserInfo: SocialUserInfo,
socialUserInfo: SocialUserInfo & {
tokenResponse?: TokenResponse;
},
options?: {
registerNewUser?: boolean;
linkSocial?: boolean;
Expand Down
21 changes: 15 additions & 6 deletions packages/integration-tests/src/tests/api/connector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,19 @@ test('connector set-up flow', async () => {
* We will test updating to the invalid connector config, that is the case not covered above.
*/
await expect(
updateConnectorConfig(connectorIdMap.get(mockSocialConnectorId)!, mockSmsConnectorConfig)
updateConnectorConfig(connectorIdMap.get(mockSocialConnectorId)!, {
config: mockSmsConnectorConfig,
})
).rejects.toThrow(HTTPError);
// To confirm the failed updating request above did not modify the original config,
// we check: the mock connector config should stay the same.
const mockSocialConnector = await getConnector(connectorIdMap.get(mockSocialConnectorId)!);
expect(mockSocialConnector.config).toEqual(mockSocialConnectorConfig);
const { config: updatedConfig } = await updateConnectorConfig(
connectorIdMap.get(mockSocialConnectorId)!,
mockSocialConnectorNewConfig
{
config: mockSocialConnectorNewConfig,
}
);
expect(updatedConfig).toEqual(mockSocialConnectorNewConfig);

Expand Down Expand Up @@ -187,10 +191,15 @@ test('create duplicated social connector', async () => {
test('override metadata for non-standard social connector', async () => {
await cleanUpConnectorTable();
const { id } = await postConnector({ connectorId: mockSocialConnectorId });
await expectRejects(updateConnectorConfig(id, {}, { target: 'target' }), {
code: 'connector.cannot_overwrite_metadata_for_non_standard_connector',
status: 400,
});
await expectRejects(
updateConnectorConfig(id, {
metadata: { target: 'target' },
}),
{
code: 'connector.cannot_overwrite_metadata_for_non_standard_connector',
status: 400,
}
);
});

test('send SMS/email test message', async () => {
Expand Down
Loading
Loading