0
Fork 0
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:
Charles Zhao 2024-09-13 14:44:15 +08:00 committed by GitHub
parent 560cb23b30
commit cf53bb2c8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 252 additions and 76 deletions

View file

@ -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 },
}) })

View file

@ -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,
}) })

View file

@ -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 },
}); });

View file

@ -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) => {

View file

@ -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 };
};

View file

@ -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 });

View file

@ -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]);

View file

@ -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,

View file

@ -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);
});
});
}); });