cypress/support/commands/testUser.ts (442 lines of code) (raw):
import {
TokenResponse,
UserResponse,
SessionResponse,
} from '@/server/models/okta/User';
import { Group } from '@/server/models/okta/Group';
import { Consent } from '@/shared/model/Consent';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
createTestUser: typeof createTestUser;
getTestOktaUser: typeof getTestOktaUser;
activateTestOktaUser: typeof activateTestOktaUser;
resetOktaUserPassword: typeof resetOktaUserPassword;
expireOktaUserPassword: typeof expireOktaUserPassword;
suspendOktaUser: typeof suspendOktaUser;
addOktaUserToGroup: typeof addOktaUserToGroup;
findEmailValidatedOktaGroupId: typeof findEmailValidatedOktaGroupId;
getOktaUserGroups: typeof getOktaUserGroups;
getTestUserDetails: typeof getTestUserDetails;
updateTestUser: typeof updateTestUser;
updateOktaTestUserProfile: typeof updateOktaTestUserProfile;
getCurrentOktaSession: typeof getCurrentOktaSession;
closeCurrentOktaSession: typeof closeCurrentOktaSession;
sendConsentEmail: typeof sendConsentEmail;
}
}
}
type Networks = 'apple' | 'google';
type SocialLink = {
socialId: number;
network: Networks;
};
type IDAPITestUserOptions = {
primaryEmailAddress?: `${string}@${string}.mailosaur.net`;
isUserEmailValidated?: boolean;
socialLinks?: SocialLink[];
password?: string;
deleteAfterMinute?: boolean;
isGuestUser?: boolean;
};
/* More fields exist in the user profile, but we only care about the ones we define in the interfaces below. */
interface IDAPIUserProfile {
id: string;
privateFields: {
firstName?: string;
secondName?: string;
};
userGroups: Array<{
path: string;
packageCode: string;
joinedDate: string;
}>;
consents: Consent[];
}
interface OktaUserProfile {
isJobsUser?: boolean;
firstName?: string;
lastName?: string;
legacyIdentityId?: string | null;
}
type IDAPITestUserResponse = [
{
key: 'GU_U';
value: string;
},
{
key: 'SC_GU_LA';
sessionCookie: boolean;
value: string;
},
{
key: 'SC_GU_U';
value: string;
},
];
export const randomMailosaurEmail = () => {
return (
crypto.randomUUID() +
'@' +
Cypress.env('MAILOSAUR_SERVER_ID') +
'.mailosaur.net'
);
};
export const randomPassword = () => crypto.randomUUID();
export const getTestUserDetails = (idapiUserId?: string) =>
cy
.request({
url: `${Cypress.env('IDAPI_BASE_URL')}/user/${idapiUserId}`,
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-GU-ID-Client-Access-Token': `Bearer ${Cypress.env(
'IDAPI_CLIENT_ACCESS_TOKEN',
)}`,
},
retryOnStatusCodeFailure: true,
})
.then((res) =>
cy.wrap({
status: res.body.status,
user: res.body.user as IDAPIUserProfile,
}),
);
export const updateTestUser = (idapiUserId: string, body: object) =>
cy
.request({
url: `${Cypress.env('IDAPI_BASE_URL')}/user/${idapiUserId}`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-GU-ID-Client-Access-Token': `Bearer ${Cypress.env(
'IDAPI_CLIENT_ACCESS_TOKEN',
)}`,
},
body: JSON.stringify(body) || undefined,
retryOnStatusCodeFailure: true,
})
.then((res) => {
cy.wrap({
status: res.body.status,
});
});
export const createTestUser = ({
primaryEmailAddress,
password,
socialLinks = [],
isUserEmailValidated = false,
deleteAfterMinute = true,
isGuestUser = false,
}: IDAPITestUserOptions) => {
// Generate a random email address if none is provided.
const finalEmail = primaryEmailAddress || randomMailosaurEmail();
// Generate a random password if none is provided.
const finalPassword = password || crypto.randomUUID();
try {
return cy
.request({
url: Cypress.env('IDAPI_BASE_URL') + '/user/test',
method: 'POST',
headers: {
'X-GU-ID-Client-Access-Token': `Bearer ${Cypress.env(
'IDAPI_CLIENT_ACCESS_TOKEN',
)}`,
},
body: {
primaryEmailAddress: finalEmail,
isUserEmailValidated,
socialLinks,
password: finalPassword,
deleteAfterMinute,
isGuestUser,
} as IDAPITestUserOptions,
retryOnStatusCodeFailure: true,
})
.then((res) => {
const cookies = res.body.values as IDAPITestUserResponse;
const guUCookie = cookies.find((cookie) => cookie.key === 'GU_U');
if (!guUCookie) {
throw new Error('Failed to create IDAPI test user');
}
const idapiUserId = JSON.parse(atob(guUCookie.value.split('.')[0]))[0];
return cy.wrap({
emailAddress: finalEmail,
cookies: res.body.values as IDAPITestUserResponse,
finalPassword,
idapiUserId,
});
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to create IDAPI test user: ' + error);
}
};
export const sendConsentEmail = ({
emailAddress,
consents,
newsletters,
}: {
emailAddress: string;
consents?: string[];
newsletters?: string[];
}) => {
try {
return cy.request({
url: Cypress.env('IDAPI_BASE_URL') + '/consent-email',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-GU-ID-Client-Access-Token': `Bearer ${Cypress.env(
'IDAPI_CLIENT_ACCESS_TOKEN',
)}`,
},
body: JSON.stringify([
{
email: emailAddress,
'set-consents': consents,
'set-lists': newsletters,
},
]),
retryOnStatusCodeFailure: true,
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to send consents email: ' + error);
}
};
export const getTestOktaUser = (id: string) => {
try {
return cy
.request({
url: `${Cypress.env('OKTA_ORG_URL')}/api/v1/users/${id}`,
method: 'GET',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
const user = res.body as UserResponse;
return cy.wrap(user);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to create Okta test user: ' + error);
}
};
export const updateOktaTestUserProfile = (
id: string,
profile: OktaUserProfile,
) => {
try {
return cy
.request({
url: `${Cypress.env('OKTA_ORG_URL')}/api/v1/users/${id}`,
method: 'POST',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
body: {
profile,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
const token = res.body as TokenResponse;
return cy.wrap(token);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to update Okta test user: ' + error);
}
};
export const activateTestOktaUser = (id: string) => {
try {
return cy
.request({
url: `${Cypress.env(
'OKTA_ORG_URL',
)}/api/v1/users/${id}/lifecycle/activate`,
method: 'POST',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
qs: {
sendEmail: false,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
const token = res.body as TokenResponse;
return cy.wrap(token);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to activate Okta test user: ' + error);
}
};
export const resetOktaUserPassword = (id: string) => {
try {
return cy
.request({
url: `${Cypress.env(
'OKTA_ORG_URL',
)}/api/v1/users/${id}/lifecycle/reset_password`,
method: 'POST',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
qs: {
sendEmail: false,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
const token = res.body.resetPasswordUrl.split('/').slice(-1)[0];
return cy.wrap({ token } as TokenResponse);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to reset password for Okta test user: ' + error);
}
};
export const expireOktaUserPassword = (id: string) => {
try {
return cy
.request({
url: `${Cypress.env(
'OKTA_ORG_URL',
)}/api/v1/users/${id}/lifecycle/expire_password`,
method: 'POST',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
const user = res.body as UserResponse;
return cy.wrap(user);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to expire password for Okta test user: ' + error);
}
};
export const suspendOktaUser = (id: string) => {
try {
return cy
.request({
url: `${Cypress.env(
'OKTA_ORG_URL',
)}/api/v1/users/${id}/lifecycle/suspend`,
method: 'POST',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
retryOnStatusCodeFailure: true,
})
.then(() => {
return cy.wrap(true);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to suspend Okta test user: ' + error);
}
};
export const getOktaUserGroups = (id: string) => {
try {
return cy
.request({
url: `${Cypress.env('OKTA_ORG_URL')}/api/v1/users/${id}/groups`,
method: 'GET',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
const user = res.body as Group[];
return cy.wrap(user);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to get user groups: ' + error);
}
};
export const addOktaUserToGroup = (id: string, groupId: string) => {
try {
return cy
.request({
url: `${Cypress.env(
'OKTA_ORG_URL',
)}/api/v1/groups/${groupId}/users/${id}`,
method: 'PUT',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
retryOnStatusCodeFailure: true,
})
.then(() => {
return cy.wrap(true);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to add Okta test user to group: ' + error);
}
};
export const findEmailValidatedOktaGroupId = () => {
try {
return cy
.request({
url: `${Cypress.env(
'OKTA_ORG_URL',
)}/api/v1/groups?q=GuardianUser-EmailValidated`,
method: 'GET',
headers: {
Authorization: `SSWS ${Cypress.env('OKTA_API_TOKEN')}`,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
const group = res.body[0]?.id as string | undefined;
if (!group) {
throw new Error('Failed to find Okta group');
}
return cy.wrap(group);
});
} catch (error) {
throw new Error(
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
'Failed to get ID of GuardianUser-EmailValidated group: ' + error,
);
}
};
export const getCurrentOktaSession = ({ idx }: { idx?: string }) => {
try {
const Cookie = `${idx ? `idx=${idx};` : ''}`;
return cy
.request({
url: `${Cypress.env('OKTA_ORG_URL')}/api/v1/sessions/me`,
method: 'GET',
headers: {
Cookie,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
const session = res.body as SessionResponse;
return cy.wrap(session);
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to get current Okta session: ' + error);
}
};
export const closeCurrentOktaSession = ({ idx }: { idx?: string }) => {
try {
const Cookie = `${idx ? `idx=${idx};` : ''}`;
return cy
.request({
url: `${Cypress.env('OKTA_ORG_URL')}/api/v1/sessions/me`,
method: 'DELETE',
headers: {
Cookie,
},
retryOnStatusCodeFailure: true,
})
.then((res) => {
if (res.status === 204 || res.status == 404) {
return true;
}
return false;
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands -- try to append, worst case our error will contain a `[object Object]` string.
throw new Error('Failed to close current Okta session: ' + error);
}
};