mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
test: add integration test for no password user setting password (#6579)
* test: add integration test for no password user setting password * refactor(test): polish content Co-authored-by: Gao Sun <gao@silverhand.io> --------- Co-authored-by: Gao Sun <gao@silverhand.io>
This commit is contained in:
parent
560cb23b30
commit
cf53bb2c8c
9 changed files with 252 additions and 76 deletions
|
@ -4,6 +4,7 @@ import type {
|
||||||
ConnectorResponse,
|
ConnectorResponse,
|
||||||
CreateConnector,
|
CreateConnector,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
import { type KyInstance } from 'ky';
|
||||||
|
|
||||||
import { authedAdminApi } from './api.js';
|
import { authedAdminApi } from './api.js';
|
||||||
|
|
||||||
|
@ -15,29 +16,32 @@ import { authedAdminApi } from './api.js';
|
||||||
* that contain metadata (considered connectors' FIXED properties) and code implementation (which determines how connectors work).
|
* that contain metadata (considered connectors' FIXED properties) and code implementation (which determines how connectors work).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const listConnectors = async () =>
|
export const listConnectors = async (api: KyInstance = authedAdminApi) =>
|
||||||
authedAdminApi.get('connectors').json<ConnectorResponse[]>();
|
api.get('connectors').json<ConnectorResponse[]>();
|
||||||
|
|
||||||
export const getConnector = async (id: string) =>
|
export const getConnector = async (id: string, api: KyInstance = authedAdminApi) =>
|
||||||
authedAdminApi.get(`connectors/${id}`).json<ConnectorResponse>();
|
api.get(`connectors/${id}`).json<ConnectorResponse>();
|
||||||
|
|
||||||
export const listConnectorFactories = async () =>
|
export const listConnectorFactories = async (api: KyInstance = authedAdminApi) =>
|
||||||
authedAdminApi.get('connector-factories').json<ConnectorFactoryResponse[]>();
|
api.get('connector-factories').json<ConnectorFactoryResponse[]>();
|
||||||
|
|
||||||
export const getConnectorFactory = async (connectorFactoryId: string) =>
|
export const getConnectorFactory = async (
|
||||||
authedAdminApi.get(`connector-factories/${connectorFactoryId}`).json<ConnectorFactoryResponse>();
|
connectorFactoryId: string,
|
||||||
|
api: KyInstance = authedAdminApi
|
||||||
|
) => api.get(`connector-factories/${connectorFactoryId}`).json<ConnectorFactoryResponse>();
|
||||||
|
|
||||||
export const postConnector = async (
|
export const postConnector = async (
|
||||||
payload: Pick<CreateConnector, 'connectorId' | 'config' | 'metadata' | 'syncProfile'>
|
payload: Pick<CreateConnector, 'connectorId' | 'config' | 'metadata' | 'syncProfile'>,
|
||||||
|
api: KyInstance = authedAdminApi
|
||||||
) =>
|
) =>
|
||||||
authedAdminApi
|
api
|
||||||
.post('connectors', {
|
.post('connectors', {
|
||||||
json: payload,
|
json: payload,
|
||||||
})
|
})
|
||||||
.json<Connector>();
|
.json<Connector>();
|
||||||
|
|
||||||
export const deleteConnectorById = async (id: string) =>
|
export const deleteConnectorById = async (id: string, api: KyInstance = authedAdminApi) =>
|
||||||
authedAdminApi.delete(`connectors/${id}`).json();
|
api.delete(`connectors/${id}`).json();
|
||||||
|
|
||||||
export const updateConnectorConfig = async (
|
export const updateConnectorConfig = async (
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -75,9 +79,10 @@ const sendTestMessage = async (
|
||||||
export const getConnectorAuthorizationUri = async (
|
export const getConnectorAuthorizationUri = async (
|
||||||
connectorId: string,
|
connectorId: string,
|
||||||
state: string,
|
state: string,
|
||||||
redirectUri: string
|
redirectUri: string,
|
||||||
|
api: KyInstance = authedAdminApi
|
||||||
) =>
|
) =>
|
||||||
authedAdminApi
|
api
|
||||||
.post(`connectors/${connectorId}/authorization-uri`, {
|
.post(`connectors/${connectorId}/authorization-uri`, {
|
||||||
json: { state, redirectUri },
|
json: { state, redirectUri },
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import type { SignInExperience } from '@logto/schemas';
|
import type { SignInExperience } from '@logto/schemas';
|
||||||
|
import { type KyInstance } from 'ky';
|
||||||
|
|
||||||
import { authedAdminApi } from './api.js';
|
import { authedAdminApi } from './api.js';
|
||||||
|
|
||||||
export const getSignInExperience = async () =>
|
export const getSignInExperience = async (api: KyInstance = authedAdminApi) =>
|
||||||
authedAdminApi.get('sign-in-exp').json<SignInExperience>();
|
api.get('sign-in-exp').json<SignInExperience>();
|
||||||
|
|
||||||
export const updateSignInExperience = async (signInExperience: Partial<SignInExperience>) =>
|
export const updateSignInExperience = async (
|
||||||
authedAdminApi
|
signInExperience: Partial<SignInExperience>,
|
||||||
|
api: KyInstance = authedAdminApi
|
||||||
|
) =>
|
||||||
|
api
|
||||||
.patch('sign-in-exp', {
|
.patch('sign-in-exp', {
|
||||||
json: signInExperience,
|
json: signInExperience,
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,32 +10,22 @@ import {
|
||||||
|
|
||||||
import MockClient from '#src/client/index.js';
|
import MockClient from '#src/client/index.js';
|
||||||
|
|
||||||
import api from '../../api/api.js';
|
|
||||||
|
|
||||||
import { experienceRoutes } from './const.js';
|
import { experienceRoutes } from './const.js';
|
||||||
|
|
||||||
type RedirectResponse = {
|
type RedirectResponse = {
|
||||||
redirectTo: string;
|
redirectTo: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const identifyUser = async (cookie: string, payload: IdentificationApiPayload) =>
|
|
||||||
api
|
|
||||||
.post(experienceRoutes.identification, {
|
|
||||||
headers: { cookie },
|
|
||||||
json: payload,
|
|
||||||
})
|
|
||||||
.json();
|
|
||||||
|
|
||||||
export class ExperienceClient extends MockClient {
|
export class ExperienceClient extends MockClient {
|
||||||
public async identifyUser(payload: IdentificationApiPayload = {}) {
|
public async identifyUser(payload: IdentificationApiPayload = {}) {
|
||||||
return api.post(experienceRoutes.identification, {
|
return this.api.post(experienceRoutes.identification, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateInteractionEvent(payload: { interactionEvent: InteractionEvent }) {
|
public async updateInteractionEvent(payload: { interactionEvent: InteractionEvent }) {
|
||||||
return api
|
return this.api
|
||||||
.put(`${experienceRoutes.prefix}/interaction-event`, {
|
.put(`${experienceRoutes.prefix}/interaction-event`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -44,7 +34,7 @@ export class ExperienceClient extends MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initInteraction(payload: CreateExperienceApiPayload) {
|
public async initInteraction(payload: CreateExperienceApiPayload) {
|
||||||
return api
|
return this.api
|
||||||
.put(experienceRoutes.prefix, {
|
.put(experienceRoutes.prefix, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -53,13 +43,13 @@ export class ExperienceClient extends MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async submitInteraction(): Promise<RedirectResponse> {
|
public override async submitInteraction(): Promise<RedirectResponse> {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.prefix}/submit`, { headers: { cookie: this.interactionCookie } })
|
.post(`${experienceRoutes.prefix}/submit`, { headers: { cookie: this.interactionCookie } })
|
||||||
.json<RedirectResponse>();
|
.json<RedirectResponse>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verifyPassword(payload: PasswordVerificationPayload) {
|
public async verifyPassword(payload: PasswordVerificationPayload) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/password`, {
|
.post(`${experienceRoutes.verification}/password`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -71,7 +61,7 @@ export class ExperienceClient extends MockClient {
|
||||||
identifier: VerificationCodeIdentifier;
|
identifier: VerificationCodeIdentifier;
|
||||||
interactionEvent: InteractionEvent;
|
interactionEvent: InteractionEvent;
|
||||||
}) {
|
}) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/verification-code`, {
|
.post(`${experienceRoutes.verification}/verification-code`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -84,7 +74,7 @@ export class ExperienceClient extends MockClient {
|
||||||
verificationId: string;
|
verificationId: string;
|
||||||
code: string;
|
code: string;
|
||||||
}) {
|
}) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/verification-code/verify`, {
|
.post(`${experienceRoutes.verification}/verification-code/verify`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -99,7 +89,7 @@ export class ExperienceClient extends MockClient {
|
||||||
state: string;
|
state: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/social/${connectorId}/authorization-uri`, {
|
.post(`${experienceRoutes.verification}/social/${connectorId}/authorization-uri`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -114,7 +104,7 @@ export class ExperienceClient extends MockClient {
|
||||||
connectorData: Record<string, unknown>;
|
connectorData: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/social/${connectorId}/verify`, {
|
.post(`${experienceRoutes.verification}/social/${connectorId}/verify`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -129,7 +119,7 @@ export class ExperienceClient extends MockClient {
|
||||||
state: string;
|
state: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/sso/${connectorId}/authorization-uri`, {
|
.post(`${experienceRoutes.verification}/sso/${connectorId}/authorization-uri`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -144,7 +134,7 @@ export class ExperienceClient extends MockClient {
|
||||||
connectorData: Record<string, unknown>;
|
connectorData: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/sso/${connectorId}/verify`, {
|
.post(`${experienceRoutes.verification}/sso/${connectorId}/verify`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -153,7 +143,7 @@ export class ExperienceClient extends MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAvailableSsoConnectors(email: string) {
|
public async getAvailableSsoConnectors(email: string) {
|
||||||
return api
|
return this.api
|
||||||
.get(`${experienceRoutes.prefix}/sso-connectors`, {
|
.get(`${experienceRoutes.prefix}/sso-connectors`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
searchParams: { email },
|
searchParams: { email },
|
||||||
|
@ -162,7 +152,7 @@ export class ExperienceClient extends MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createTotpSecret() {
|
public async createTotpSecret() {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/totp/secret`, {
|
.post(`${experienceRoutes.verification}/totp/secret`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
})
|
})
|
||||||
|
@ -170,7 +160,7 @@ export class ExperienceClient extends MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verifyTotp(payload: { verificationId?: string; code: string }) {
|
public async verifyTotp(payload: { verificationId?: string; code: string }) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/totp/verify`, {
|
.post(`${experienceRoutes.verification}/totp/verify`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -179,7 +169,7 @@ export class ExperienceClient extends MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async generateMfaBackupCodes() {
|
public async generateMfaBackupCodes() {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/backup-code/generate`, {
|
.post(`${experienceRoutes.verification}/backup-code/generate`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
})
|
})
|
||||||
|
@ -187,7 +177,7 @@ export class ExperienceClient extends MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verifyBackupCode(payload: { code: string }) {
|
public async verifyBackupCode(payload: { code: string }) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/backup-code/verify`, {
|
.post(`${experienceRoutes.verification}/backup-code/verify`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -198,7 +188,7 @@ export class ExperienceClient extends MockClient {
|
||||||
public async createNewPasswordIdentityVerification(
|
public async createNewPasswordIdentityVerification(
|
||||||
payload: Pick<PasswordVerificationPayload, 'identifier'> & { password?: string }
|
payload: Pick<PasswordVerificationPayload, 'identifier'> & { password?: string }
|
||||||
) {
|
) {
|
||||||
return api
|
return this.api
|
||||||
.post(`${experienceRoutes.verification}/new-password-identity`, {
|
.post(`${experienceRoutes.verification}/new-password-identity`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
|
@ -207,27 +197,27 @@ export class ExperienceClient extends MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resetPassword(payload: { password: string }) {
|
public async resetPassword(payload: { password: string }) {
|
||||||
return api.put(`${experienceRoutes.profile}/password`, {
|
return this.api.put(`${experienceRoutes.profile}/password`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateProfile(payload: UpdateProfileApiPayload) {
|
public async updateProfile(payload: UpdateProfileApiPayload) {
|
||||||
return api.post(`${experienceRoutes.profile}`, {
|
return this.api.post(`${experienceRoutes.profile}`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: payload,
|
json: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async skipMfaBinding() {
|
public async skipMfaBinding() {
|
||||||
return api.post(`${experienceRoutes.mfa}/mfa-skipped`, {
|
return this.api.post(`${experienceRoutes.mfa}/mfa-skipped`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bindMfa(type: MfaFactor, verificationId: string) {
|
public async bindMfa(type: MfaFactor, verificationId: string) {
|
||||||
return api.post(`${experienceRoutes.mfa}`, {
|
return this.api.post(`${experienceRoutes.mfa}`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
json: { type, verificationId },
|
json: { type, verificationId },
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,14 +20,14 @@ export default class MockClient {
|
||||||
protected readonly config: LogtoConfig;
|
protected readonly config: LogtoConfig;
|
||||||
protected readonly storage: MemoryStorage;
|
protected readonly storage: MemoryStorage;
|
||||||
protected readonly logto: LogtoClient;
|
protected readonly logto: LogtoClient;
|
||||||
|
protected readonly api: KyInstance;
|
||||||
|
|
||||||
private navigateUrl?: string;
|
private navigateUrl?: string;
|
||||||
private readonly api: KyInstance;
|
|
||||||
|
|
||||||
constructor(config?: Partial<LogtoConfig>) {
|
constructor(config?: Partial<LogtoConfig>, api?: KyInstance) {
|
||||||
this.storage = new MemoryStorage();
|
this.storage = new MemoryStorage();
|
||||||
this.config = { ...defaultConfig, ...config };
|
this.config = { ...defaultConfig, ...config };
|
||||||
this.api = ky.extend({ prefixUrl: this.config.endpoint + '/api' });
|
this.api = api ?? ky.extend({ prefixUrl: this.config.endpoint + '/api' });
|
||||||
|
|
||||||
this.logto = new LogtoClient(this.config, {
|
this.logto = new LogtoClient(this.config, {
|
||||||
navigate: (url: string) => {
|
navigate: (url: string) => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// To refactor: should combine into other similar utils
|
// To refactor: should combine into other similar utils
|
||||||
// Since they are just different in URLs
|
// Since they are just different in URLs
|
||||||
|
|
||||||
|
import { type SocialUserInfo } from '@logto/connector-kit';
|
||||||
import type { LogtoConfig } from '@logto/node';
|
import type { LogtoConfig } from '@logto/node';
|
||||||
import {
|
import {
|
||||||
PredefinedScope,
|
PredefinedScope,
|
||||||
|
@ -17,9 +18,14 @@ import {
|
||||||
import { authedAdminTenantApi as api, adminTenantApi } from '#src/api/api.js';
|
import { authedAdminTenantApi as api, adminTenantApi } from '#src/api/api.js';
|
||||||
import type { InteractionPayload } from '#src/api/interaction.js';
|
import type { InteractionPayload } from '#src/api/interaction.js';
|
||||||
import { adminConsoleRedirectUri, logtoConsoleUrl } from '#src/constants.js';
|
import { adminConsoleRedirectUri, logtoConsoleUrl } from '#src/constants.js';
|
||||||
import { initClient } from '#src/helpers/client.js';
|
import { initClient, initExperienceClient, processSession } from '#src/helpers/client.js';
|
||||||
import { generatePassword, generateUsername } from '#src/utils.js';
|
import { generatePassword, generateUsername } from '#src/utils.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
successFullyCreateSocialVerification,
|
||||||
|
successFullyVerifySocialAuthorization,
|
||||||
|
} from './experience/social-verification.js';
|
||||||
|
|
||||||
export const resourceDefault = getManagementApiResourceIndicator(defaultTenantId);
|
export const resourceDefault = getManagementApiResourceIndicator(defaultTenantId);
|
||||||
export const resourceMe = getManagementApiResourceIndicator(adminTenantId, 'me');
|
export const resourceMe = getManagementApiResourceIndicator(adminTenantId, 'me');
|
||||||
|
|
||||||
|
@ -67,6 +73,15 @@ export const putInteraction = async (cookie: string, payload: InteractionPayload
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
|
export const initAdminExperienceClient = async (config?: Partial<LogtoConfig>) =>
|
||||||
|
initExperienceClient(
|
||||||
|
InteractionEvent.SignIn,
|
||||||
|
{ endpoint: logtoConsoleUrl, appId: adminConsoleApplicationId, ...config },
|
||||||
|
adminConsoleRedirectUri,
|
||||||
|
undefined,
|
||||||
|
adminTenantApi
|
||||||
|
);
|
||||||
|
|
||||||
export const initClientAndSignIn = async (
|
export const initClientAndSignIn = async (
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
|
@ -102,3 +117,56 @@ export const createUserWithAllRolesAndSignInToClient = async () => {
|
||||||
|
|
||||||
return { id, client, username, password };
|
return { id, client, username, password };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const signUpWithSocialAndSignInToClient = async (
|
||||||
|
connectorId: string,
|
||||||
|
socialUserInfo: SocialUserInfo
|
||||||
|
) => {
|
||||||
|
const state = 'state';
|
||||||
|
const redirectUri = 'http://localhost:3000';
|
||||||
|
|
||||||
|
const client = await initAdminExperienceClient({
|
||||||
|
resources: [resourceDefault, resourceMe],
|
||||||
|
scopes: [PredefinedScope.All],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { verificationId } = await successFullyCreateSocialVerification(client, connectorId, {
|
||||||
|
redirectUri,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id, ...rest } = socialUserInfo;
|
||||||
|
|
||||||
|
await successFullyVerifySocialAuthorization(client, connectorId, {
|
||||||
|
verificationId,
|
||||||
|
connectorData: {
|
||||||
|
state,
|
||||||
|
redirectUri,
|
||||||
|
code: 'fake_code',
|
||||||
|
userId: socialUserInfo.id,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.updateInteractionEvent({ interactionEvent: InteractionEvent.Register });
|
||||||
|
await client.identifyUser({ verificationId });
|
||||||
|
|
||||||
|
const { redirectTo } = await client.submitInteraction();
|
||||||
|
const userId = await processSession(client, redirectTo);
|
||||||
|
const existingUserRoles = await api.get(`users/${userId}/roles`).json<Role[]>();
|
||||||
|
const existingUserRoleIds = new Set(existingUserRoles.map(({ id }) => id));
|
||||||
|
|
||||||
|
// Should have roles for default tenant Management API and admin tenant Me API
|
||||||
|
const roles = await api.get('roles').json<Role[]>();
|
||||||
|
await Promise.all(
|
||||||
|
roles
|
||||||
|
.filter(({ id, type }) => !existingUserRoleIds.has(id) && type !== RoleType.MachineToMachine)
|
||||||
|
.map(async ({ id }) =>
|
||||||
|
api.post(`roles/${id}/users`, {
|
||||||
|
json: { userIds: [userId] },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { id: userId, client };
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { LogtoConfig, SignInOptions } from '@logto/node';
|
import type { LogtoConfig, SignInOptions } from '@logto/node';
|
||||||
import { InteractionEvent } from '@logto/schemas';
|
import { InteractionEvent } from '@logto/schemas';
|
||||||
import { assert } from '@silverhand/essentials';
|
import { assert } from '@silverhand/essentials';
|
||||||
|
import { type KyInstance } from 'ky';
|
||||||
|
|
||||||
import { ExperienceClient } from '#src/client/experience/index.js';
|
import { ExperienceClient } from '#src/client/experience/index.js';
|
||||||
import MockClient from '#src/client/index.js';
|
import MockClient from '#src/client/index.js';
|
||||||
|
@ -21,9 +22,10 @@ export const initExperienceClient = async (
|
||||||
interactionEvent: InteractionEvent = InteractionEvent.SignIn,
|
interactionEvent: InteractionEvent = InteractionEvent.SignIn,
|
||||||
config?: Partial<LogtoConfig>,
|
config?: Partial<LogtoConfig>,
|
||||||
redirectUri?: string,
|
redirectUri?: string,
|
||||||
options: Omit<SignInOptions, 'redirectUri'> = {}
|
options: Omit<SignInOptions, 'redirectUri'> = {},
|
||||||
|
api?: KyInstance
|
||||||
) => {
|
) => {
|
||||||
const client = new ExperienceClient(config);
|
const client = new ExperienceClient(config, api);
|
||||||
await client.initSession(redirectUri, options);
|
await client.initSession(redirectUri, options);
|
||||||
assert(client.interactionCookie, new Error('Session not found'));
|
assert(client.interactionCookie, new Error('Session not found'));
|
||||||
await client.initInteraction({ interactionEvent });
|
await client.initInteraction({ interactionEvent });
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ConnectorType } from '@logto/schemas';
|
import { ConnectorType } from '@logto/schemas';
|
||||||
|
import { type KyInstance } from 'ky';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
mockEmailConnectorConfig,
|
mockEmailConnectorConfig,
|
||||||
|
@ -11,10 +12,12 @@ import {
|
||||||
import { deleteConnectorById, listConnectors, postConnector } from '#src/api/index.js';
|
import { deleteConnectorById, listConnectors, postConnector } from '#src/api/index.js';
|
||||||
import { deleteSsoConnectorById, getSsoConnectors } from '#src/api/sso-connector.js';
|
import { deleteSsoConnectorById, getSsoConnectors } from '#src/api/sso-connector.js';
|
||||||
|
|
||||||
export const clearConnectorsByTypes = async (types: ConnectorType[]) => {
|
export const clearConnectorsByTypes = async (types: ConnectorType[], api?: KyInstance) => {
|
||||||
const connectors = await listConnectors();
|
const connectors = await listConnectors(api);
|
||||||
const targetConnectors = connectors.filter((connector) => types.includes(connector.type));
|
const targetConnectors = connectors.filter((connector) => types.includes(connector.type));
|
||||||
await Promise.all(targetConnectors.map(async (connector) => deleteConnectorById(connector.id)));
|
await Promise.all(
|
||||||
|
targetConnectors.map(async (connector) => deleteConnectorById(connector.id, api))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearSsoConnectors = async () => {
|
export const clearSsoConnectors = async () => {
|
||||||
|
@ -24,24 +27,33 @@ export const clearSsoConnectors = async () => {
|
||||||
|
|
||||||
export const clearConnectorById = async (id: string) => deleteConnectorById(id);
|
export const clearConnectorById = async (id: string) => deleteConnectorById(id);
|
||||||
|
|
||||||
export const setEmailConnector = async () =>
|
export const setEmailConnector = async (api?: KyInstance) =>
|
||||||
postConnector({
|
postConnector(
|
||||||
|
{
|
||||||
connectorId: mockEmailConnectorId,
|
connectorId: mockEmailConnectorId,
|
||||||
config: mockEmailConnectorConfig,
|
config: mockEmailConnectorConfig,
|
||||||
});
|
},
|
||||||
|
api
|
||||||
|
);
|
||||||
|
|
||||||
export const setSmsConnector = async () =>
|
export const setSmsConnector = async (api?: KyInstance) =>
|
||||||
postConnector({
|
postConnector(
|
||||||
|
{
|
||||||
connectorId: mockSmsConnectorId,
|
connectorId: mockSmsConnectorId,
|
||||||
config: mockSmsConnectorConfig,
|
config: mockSmsConnectorConfig,
|
||||||
});
|
},
|
||||||
|
api
|
||||||
|
);
|
||||||
|
|
||||||
export const setSocialConnector = async () =>
|
export const setSocialConnector = async (api?: KyInstance) =>
|
||||||
postConnector({
|
postConnector(
|
||||||
|
{
|
||||||
connectorId: mockSocialConnectorId,
|
connectorId: mockSocialConnectorId,
|
||||||
config: mockSocialConnectorConfig,
|
config: mockSocialConnectorConfig,
|
||||||
syncProfile: true,
|
syncProfile: true,
|
||||||
});
|
},
|
||||||
|
api
|
||||||
|
);
|
||||||
|
|
||||||
export const resetPasswordlessConnectors = async () => {
|
export const resetPasswordlessConnectors = async () => {
|
||||||
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
|
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
|
||||||
|
|
|
@ -5,13 +5,32 @@ import { updateSignInExperience } from '#src/api/index.js';
|
||||||
|
|
||||||
import { clearConnectorsByTypes } from './connector.js';
|
import { clearConnectorsByTypes } from './connector.js';
|
||||||
|
|
||||||
|
export const defaultSignInSignUpConfigs = {
|
||||||
|
signInMode: SignInMode.SignInAndRegister,
|
||||||
|
signUp: {
|
||||||
|
identifiers: [SignInIdentifier.Username],
|
||||||
|
password: true,
|
||||||
|
verify: false,
|
||||||
|
},
|
||||||
|
signIn: {
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
identifier: SignInIdentifier.Username,
|
||||||
|
isPasswordPrimary: true,
|
||||||
|
password: true,
|
||||||
|
verificationCode: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const defaultSignUpMethod = {
|
export const defaultSignUpMethod = {
|
||||||
identifiers: [],
|
identifiers: [],
|
||||||
password: false,
|
password: false,
|
||||||
verify: false,
|
verify: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultPasswordSignInMethods = [
|
export const defaultPasswordSignInMethods = [
|
||||||
{
|
{
|
||||||
identifier: SignInIdentifier.Username,
|
identifier: SignInIdentifier.Username,
|
||||||
password: true,
|
password: true,
|
||||||
|
|
|
@ -1,14 +1,26 @@
|
||||||
|
import { ConnectorType } from '@logto/connector-kit';
|
||||||
|
import { SignInMode, SignInIdentifier } from '@logto/schemas';
|
||||||
|
import { generateStandardId } from '@logto/shared';
|
||||||
import ky from 'ky';
|
import ky from 'ky';
|
||||||
|
|
||||||
|
import { authedAdminTenantApi as api } from '#src/api/api.js';
|
||||||
|
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||||
import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
||||||
import {
|
import {
|
||||||
createUserWithAllRolesAndSignInToClient,
|
createUserWithAllRolesAndSignInToClient,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
resourceDefault,
|
resourceDefault,
|
||||||
resourceMe,
|
resourceMe,
|
||||||
|
signUpWithSocialAndSignInToClient,
|
||||||
} from '#src/helpers/admin-tenant.js';
|
} from '#src/helpers/admin-tenant.js';
|
||||||
|
import {
|
||||||
|
clearConnectorsByTypes,
|
||||||
|
setEmailConnector,
|
||||||
|
setSocialConnector,
|
||||||
|
} from '#src/helpers/connector.js';
|
||||||
import { expectRejects } from '#src/helpers/index.js';
|
import { expectRejects } from '#src/helpers/index.js';
|
||||||
import { generatePassword } from '#src/utils.js';
|
import { defaultSignInSignUpConfigs } from '#src/helpers/sign-in-experience.js';
|
||||||
|
import { generateEmail, generatePassword } from '#src/utils.js';
|
||||||
|
|
||||||
describe('me', () => {
|
describe('me', () => {
|
||||||
it('should only be available in admin tenant', async () => {
|
it('should only be available in admin tenant', async () => {
|
||||||
|
@ -100,4 +112,68 @@ describe('me', () => {
|
||||||
|
|
||||||
await deleteUser(id);
|
await deleteUser(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('social sign-up user who has no password', () => {
|
||||||
|
const context = new (class Context {
|
||||||
|
socialConnectorId?: string;
|
||||||
|
userId?: string;
|
||||||
|
})();
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Prepare social sign-up and sign-in configs in SIE
|
||||||
|
await clearConnectorsByTypes([ConnectorType.Social, ConnectorType.Email], api);
|
||||||
|
const { id: socialConnectorId } = await setSocialConnector(api);
|
||||||
|
await setEmailConnector(api);
|
||||||
|
await updateSignInExperience(
|
||||||
|
{
|
||||||
|
signInMode: SignInMode.SignInAndRegister,
|
||||||
|
signUp: {
|
||||||
|
identifiers: [SignInIdentifier.Email],
|
||||||
|
password: false,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
signIn: {
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
identifier: SignInIdentifier.Email,
|
||||||
|
password: false,
|
||||||
|
verificationCode: true,
|
||||||
|
isPasswordPrimary: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
api
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||||
|
context.socialConnectorId = socialConnectorId;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Clean up
|
||||||
|
await updateSignInExperience(defaultSignInSignUpConfigs, api);
|
||||||
|
await clearConnectorsByTypes([ConnectorType.Social, ConnectorType.Email], api);
|
||||||
|
await deleteUser(context.userId!);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow no-password user to set password', async () => {
|
||||||
|
const { id: userId, client } = await signUpWithSocialAndSignInToClient(
|
||||||
|
context.socialConnectorId!,
|
||||||
|
{
|
||||||
|
id: generateStandardId(),
|
||||||
|
email: generateEmail(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||||
|
context.userId = userId;
|
||||||
|
|
||||||
|
const headers = { authorization: `Bearer ${await client.getAccessToken(resourceMe)}` };
|
||||||
|
await expect(
|
||||||
|
ky.post(logtoConsoleUrl + '/me/password', {
|
||||||
|
headers,
|
||||||
|
json: { password: generatePassword() },
|
||||||
|
})
|
||||||
|
).resolves.toHaveProperty('status', 204);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue