Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
1619 lines
44 KiB
TypeScript
1619 lines
44 KiB
TypeScript
// Copyright © 2023 Ory Corp
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
import {
|
||
APP_URL,
|
||
assertVerifiableAddress,
|
||
extractOTPCode,
|
||
gen,
|
||
KRATOS_ADMIN,
|
||
KRATOS_PUBLIC,
|
||
MAIL_API,
|
||
MOBILE_URL,
|
||
parseHtml,
|
||
pollInterval,
|
||
} from "../helpers"
|
||
|
||
import dayjs from "dayjs"
|
||
import YAML from "yamljs"
|
||
import { MailMessage, Strategy } from "."
|
||
import { OryKratosConfiguration } from "../../shared/config"
|
||
import { UiNode } from "@ory/kratos-client"
|
||
import { ConfigBuilder } from "./configHelpers"
|
||
|
||
const configFile = "kratos.generated.yml"
|
||
|
||
const mergeFields = (form, fields) => {
|
||
const result = {}
|
||
form.nodes.forEach(({ attributes, type }) => {
|
||
if (type === "input") {
|
||
result[attributes.name] = attributes.value
|
||
}
|
||
})
|
||
|
||
return { ...result, ...fields }
|
||
}
|
||
|
||
function checkConfigVersion(previous, tries = 0) {
|
||
cy.wait(50)
|
||
cy.request("GET", KRATOS_ADMIN + "/health/config").then(({ body }) => {
|
||
if (previous !== body) {
|
||
return
|
||
} else if (tries > 8) {
|
||
console.warn(
|
||
"Config version did not change after 5 tries, maybe the changes did not have an effect?",
|
||
)
|
||
return
|
||
}
|
||
cy.wait(50)
|
||
checkConfigVersion(previous, tries + 1)
|
||
})
|
||
}
|
||
|
||
const updateConfigFile = (
|
||
cb: (arg: OryKratosConfiguration) => OryKratosConfiguration,
|
||
) => {
|
||
cy.request("GET", KRATOS_ADMIN + "/health/config").then(({ body }) => {
|
||
cy.readFile(configFile).then((contents) => {
|
||
cy.writeFile(configFile, YAML.stringify(cb(YAML.parse(contents))))
|
||
cy.wait(500)
|
||
})
|
||
checkConfigVersion(body)
|
||
})
|
||
}
|
||
|
||
Cypress.Commands.add("useConfigProfile", (profile: string) => {
|
||
cy.request("GET", KRATOS_ADMIN + "/health/config").then(({ body }) => {
|
||
console.log("Switching config profile to:", profile)
|
||
cy.readFile(`kratos.${profile}.yml`).then((contents) =>
|
||
cy.writeFile(configFile, contents),
|
||
)
|
||
checkConfigVersion(body)
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("proxy", (app: string) => {
|
||
console.log("Switching proxy profile to:", app)
|
||
cy.writeFile(`proxy.json`, `"${app}"`)
|
||
cy.request(APP_URL + "/")
|
||
.its("body", { log: false })
|
||
.then((body) => {
|
||
expect(body.indexOf(`data-testid="app-${app}"`) > -1).to.be.true
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("shortPrivilegedSessionTime", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.settings.privileged_session_max_age = "1ms"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("setIdentitySchema", (schema: string) => {
|
||
updateConfigFile((config) => {
|
||
const id = gen.password()
|
||
config.identity.default_schema_id = id
|
||
config.identity.schemas = [
|
||
...(config.identity.schemas || []),
|
||
{
|
||
id,
|
||
url: schema,
|
||
},
|
||
]
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("setDefaultIdentitySchema", (id: string) => {
|
||
updateConfigFile((config) => {
|
||
config.identity.default_schema_id = id
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("longPrivilegedSessionTime", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.settings.privileged_session_max_age = "5m"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("longVerificationLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.verification.lifespan = "1m"
|
||
return config
|
||
})
|
||
})
|
||
Cypress.Commands.add("shortVerificationLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.verification.lifespan = "1ms"
|
||
return config
|
||
})
|
||
})
|
||
Cypress.Commands.add("sessionRequiresNo2fa", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.session.whoami.required_aal = "aal1"
|
||
return config
|
||
})
|
||
})
|
||
Cypress.Commands.add("sessionRequires2fa", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.session.whoami.required_aal = "highest_available"
|
||
return config
|
||
})
|
||
})
|
||
Cypress.Commands.add("shortLinkLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods.link.config.lifespan = "1ms"
|
||
return config
|
||
})
|
||
})
|
||
Cypress.Commands.add("longLinkLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods.link.config.lifespan = "1m"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("shortCodeLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods.code.config.lifespan = "1ms"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("shortLifespan", (strategy: Strategy) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods[strategy].config.lifespan = "1ms"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("longLifespan", (strategy: Strategy) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods[strategy].config.lifespan = "1m"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("longCodeLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods.code.config.lifespan = "1m"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("shortCodeLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods.code.config.lifespan = "1ms"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("longCodeLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods.code.config.lifespan = "1m"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("longRecoveryLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.recovery.lifespan = "1m"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("enableLoginForVerifiedAddressOnly", () => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.login["after"] = {
|
||
password: { hooks: [{ hook: "require_verified_address" }] },
|
||
}
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("setupHooks", (flow, phase, kind, hooks) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows[flow][phase][kind] = { hooks }
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("setPostPasswordRegistrationHooks", (hooks) => {
|
||
cy.setupHooks("registration", "after", "password", hooks)
|
||
})
|
||
|
||
Cypress.Commands.add("setPostCodeRegistrationHooks", (hooks) => {
|
||
cy.setupHooks("registration", "after", "code", hooks)
|
||
})
|
||
|
||
Cypress.Commands.add("shortLoginLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.login.lifespan = "100ms"
|
||
return config
|
||
})
|
||
})
|
||
Cypress.Commands.add("longLoginLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.login.lifespan = "1h"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("shortRecoveryLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.recovery.lifespan = "1ms"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("requireStrictAal", () => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.settings.required_aal = "highest_available"
|
||
config.session.whoami.required_aal = "highest_available"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("useLaxAal", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.settings.required_aal = "aal1"
|
||
config.session.whoami.required_aal = "aal1"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("disableVerification", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.verification.enabled = false
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("enableVerification", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.verification.enabled = true
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("enableRecovery", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
if (!config.selfservice.flows.recovery) {
|
||
config.selfservice.flows.recovery = {}
|
||
}
|
||
config.selfservice.flows.recovery.enabled = true
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("useRecoveryStrategy", (strategy: Strategy) => {
|
||
updateConfigFile((config) => {
|
||
if (!config.selfservice.flows.recovery) {
|
||
config.selfservice.flows.recovery = {}
|
||
}
|
||
config.selfservice.flows.recovery.use = strategy
|
||
if (!config.selfservice.methods[strategy]) {
|
||
config.selfservice.methods[strategy] = {}
|
||
}
|
||
config.selfservice.methods[strategy].enabled = true
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("disableRecoveryStrategy", (strategy: Strategy) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.methods[strategy].enabled = false
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("disableRecovery", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.recovery.enabled = false
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("disableRegistration", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.registration.enabled = false
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("enableRegistration", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.registration.enabled = true
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("useLaxAal", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.settings.required_aal = "aal1"
|
||
config.session.whoami.required_aal = "aal1"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("updateConfigFile", (cb: (arg: any) => any) => {
|
||
updateConfigFile(cb)
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"register",
|
||
({
|
||
email = gen.email(),
|
||
password = gen.password(),
|
||
query = {},
|
||
fields = {},
|
||
} = {}) => {
|
||
console.log("Creating user account: ", { email, password })
|
||
|
||
const cookieDomain = new URL(APP_URL).hostname
|
||
// Somehow, this line makes the clearCookies call work, without it, the cookies are not always cleared and the test fails randomly. Make it make sense.
|
||
cy.getCookies({
|
||
domain: cookieDomain,
|
||
})
|
||
cy.clearCookies({
|
||
domain: cookieDomain,
|
||
})
|
||
|
||
cy.request({
|
||
url: APP_URL + "/self-service/registration/browser",
|
||
followRedirect: false,
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
qs: query,
|
||
})
|
||
.then(({ body, status }) => {
|
||
expect(status).to.eq(200)
|
||
const form = body.ui
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, {
|
||
...fields,
|
||
"traits.email": email,
|
||
password,
|
||
method: "password",
|
||
}),
|
||
url: form.action,
|
||
followRedirect: false,
|
||
})
|
||
})
|
||
.then(({ body }) => {
|
||
expect(body.identity.traits.email).to.contain(email)
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"registerWithCode",
|
||
({
|
||
email = gen.email(),
|
||
code = undefined,
|
||
traits = {},
|
||
query = {},
|
||
expectedMailCount = 1,
|
||
} = {}) => {
|
||
const cookieDomain = new URL(APP_URL).hostname
|
||
cy.clearCookies({
|
||
domain: cookieDomain,
|
||
})
|
||
cy.getCookies({
|
||
domain: cookieDomain,
|
||
}).should("be.empty") // assert cookies are actually empty
|
||
|
||
cy.request({
|
||
url: APP_URL + "/self-service/registration/browser",
|
||
method: "GET",
|
||
followRedirect: false,
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
qs: query || {},
|
||
}).then(({ body, status }) => {
|
||
expect(status).to.eq(200)
|
||
const form = body.ui
|
||
return cy
|
||
.request({
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
method: form.method,
|
||
body: mergeFields(form, {
|
||
method: "code",
|
||
"traits.email": email,
|
||
...traits,
|
||
...(code && { code }),
|
||
}),
|
||
url: form.action,
|
||
followRedirect: false,
|
||
failOnStatusCode: false,
|
||
})
|
||
.then(({ body }) => {
|
||
if (!code) {
|
||
expect(
|
||
body.ui.nodes.find(
|
||
(f: UiNode) =>
|
||
f.group === "default" &&
|
||
"name" in f.attributes &&
|
||
f.attributes.name === "traits.email",
|
||
)?.attributes.value,
|
||
).to.eq(email)
|
||
|
||
return cy
|
||
.getRegistrationCodeFromEmail(email, {
|
||
expectedCount: expectedMailCount,
|
||
})
|
||
.then((code) => {
|
||
return cy.request({
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
method: form.method,
|
||
body: mergeFields(form, {
|
||
method: "code",
|
||
"traits.email": email,
|
||
code,
|
||
...traits,
|
||
}),
|
||
url: form.action,
|
||
followRedirect: false,
|
||
})
|
||
})
|
||
} else {
|
||
expect(body.session).to.contain(email)
|
||
}
|
||
})
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"registerApi",
|
||
({ email = gen.email(), password = gen.password(), fields = {} } = {}) =>
|
||
cy
|
||
.request({
|
||
url: APP_URL + "/self-service/registration/api",
|
||
})
|
||
.then(({ body }) => {
|
||
const form = body.ui
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, {
|
||
...fields,
|
||
"traits.email": email,
|
||
password,
|
||
method: "password",
|
||
}),
|
||
url: form.action,
|
||
})
|
||
})
|
||
.then(({ body }) => {
|
||
expect(body.identity.traits.email).to.contain(email)
|
||
return body
|
||
}),
|
||
)
|
||
|
||
Cypress.Commands.add("settingsApi", ({ fields = {} } = {}) =>
|
||
cy
|
||
.request({
|
||
url: APP_URL + "/self-service/settings/api",
|
||
})
|
||
.then(({ body }) => {
|
||
const form = body.ui
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, {
|
||
...fields,
|
||
}),
|
||
url: form.action,
|
||
})
|
||
})
|
||
.then(({ body }) => {
|
||
expect(body.statusCode).to.eq(200)
|
||
}),
|
||
)
|
||
|
||
Cypress.Commands.add("loginApi", ({ email, password } = {}) =>
|
||
cy
|
||
.request({
|
||
url: APP_URL + "/self-service/login/api",
|
||
})
|
||
.then(({ body }) => {
|
||
const form = body.ui
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, {
|
||
identifier: email,
|
||
password,
|
||
method: "password",
|
||
}),
|
||
url: form.action,
|
||
})
|
||
})
|
||
.then(({ body }) => {
|
||
expect(body.session.identity.traits.email).to.contain(email)
|
||
return body
|
||
}),
|
||
)
|
||
|
||
Cypress.Commands.add("loginApiWithoutCookies", ({ email, password } = {}) => {
|
||
cy.task("httpRequest", {
|
||
url: APP_URL + "/self-service/login/api",
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
responseType: "json",
|
||
}).then((body: any) => {
|
||
cy.task("httpRequest", {
|
||
method: body.ui.method,
|
||
json: mergeFields(body.ui, {
|
||
identifier: email,
|
||
password,
|
||
method: "password",
|
||
}),
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
responseType: "json",
|
||
url: body.ui.action,
|
||
}).then((body: any) => {
|
||
expect(body.session.identity.traits.email).to.contain(email)
|
||
return body
|
||
})
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("recoverApi", ({ email, returnTo }) => {
|
||
let url = APP_URL + "/self-service/recovery/api"
|
||
if (returnTo) {
|
||
url += "?return_to=" + returnTo
|
||
}
|
||
cy.request({ url })
|
||
.then(({ body }) => {
|
||
const form = body.ui
|
||
// label should still exist after request, for more detail: #2591
|
||
expect(form.nodes[1].meta).to.not.be.null
|
||
expect(form.nodes[1].meta.label).to.not.be.null
|
||
expect(form.nodes[1].meta.label.text).to.equal("Email")
|
||
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, { email, method: "link" }),
|
||
url: form.action,
|
||
})
|
||
})
|
||
.then(({ body }) => {
|
||
expect(body.state).to.contain("sent_email")
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"verificationApi",
|
||
({ email, returnTo, strategy = "code" }) => {
|
||
let url = APP_URL + "/self-service/verification/api"
|
||
if (returnTo) {
|
||
url += "?return_to=" + returnTo
|
||
}
|
||
cy.request({ url })
|
||
.then(({ body }) => {
|
||
const form = body.ui
|
||
expect(form.nodes.some((node) => node.meta?.label?.text === "Email")).to
|
||
.be.true
|
||
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, { email, method: strategy }),
|
||
url: form.action,
|
||
headers: {
|
||
Accept: "application/json", // "Emulate" an API client, as kratos responds with a redirect otherwise
|
||
},
|
||
})
|
||
})
|
||
.then(({ body }) => {
|
||
expect(body.state).to.contain("sent_email")
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"verificationApiExpired",
|
||
({ email, returnTo, strategy = "code" }) => {
|
||
cy.shortVerificationLifespan()
|
||
let url = APP_URL + "/self-service/verification/api"
|
||
if (returnTo) {
|
||
url += "?return_to=" + returnTo
|
||
}
|
||
cy.request({ url })
|
||
.then(({ body }) => {
|
||
const form = body.ui
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, { email, method: strategy }),
|
||
url: form.action,
|
||
failOnStatusCode: false,
|
||
})
|
||
})
|
||
.then((response) => {
|
||
expect(response.status).to.eq(410)
|
||
expect(response.body.error.reason).to.eq(
|
||
"The verification flow has expired. Redirect the user to the verification flow init endpoint to initialize a new verification flow.",
|
||
)
|
||
expect(response.body.error.details.redirect_to).to.eq(
|
||
"http://localhost:4455/self-service/verification/browser",
|
||
)
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("verificationBrowser", ({ email, returnTo }) => {
|
||
let url = APP_URL + "/self-service/verification/browser"
|
||
if (returnTo) {
|
||
url += "?return_to=" + returnTo
|
||
}
|
||
cy.request({ url })
|
||
.then(({ body }) => {
|
||
const form = body.ui
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, { email, method: "link" }),
|
||
url: form.action,
|
||
})
|
||
})
|
||
.then(({ body }) => {
|
||
expect(body.state).to.contain("sent_email")
|
||
})
|
||
})
|
||
Cypress.Commands.add("addVirtualAuthenticator", () =>
|
||
cy
|
||
.task("sendCRI", {
|
||
query: "WebAuthn.enable",
|
||
opts: {},
|
||
})
|
||
.then(() =>
|
||
cy.task("sendCRI", {
|
||
query: "WebAuthn.addVirtualAuthenticator",
|
||
opts: {
|
||
options: {
|
||
protocol: "ctap2",
|
||
transport: "usb",
|
||
hasResidentKey: true,
|
||
hasUserVerification: true,
|
||
isUserVerified: true,
|
||
},
|
||
},
|
||
}),
|
||
),
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"registerOidc",
|
||
({
|
||
app,
|
||
email,
|
||
website,
|
||
scopes,
|
||
rememberLogin = true,
|
||
rememberConsent = true,
|
||
acceptLogin = true,
|
||
acceptConsent = true,
|
||
expectSession = true,
|
||
route = APP_URL + "/registration",
|
||
}) => {
|
||
cy.visit(route)
|
||
|
||
cy.triggerOidc(app)
|
||
|
||
cy.get("#username").type(email)
|
||
if (rememberLogin) {
|
||
cy.get("#remember").click()
|
||
}
|
||
if (acceptLogin) {
|
||
cy.get("#accept").click()
|
||
} else {
|
||
cy.get("#reject").click()
|
||
}
|
||
|
||
if (scopes) {
|
||
scopes.forEach((scope) => {
|
||
cy.get("#" + scope).click()
|
||
})
|
||
} else {
|
||
cy.get('input[name="scope"]').each(($el) => cy.wrap($el).click())
|
||
}
|
||
|
||
if (website) {
|
||
cy.get("#website").clear().type(website)
|
||
}
|
||
|
||
if (rememberConsent) {
|
||
cy.get("#remember").click()
|
||
}
|
||
|
||
if (acceptConsent) {
|
||
cy.get("#accept").click()
|
||
} else {
|
||
cy.get("#reject").click()
|
||
}
|
||
|
||
cy.location("pathname").should("not.include", "consent")
|
||
|
||
if (expectSession) {
|
||
cy.getSession()
|
||
} else {
|
||
cy.noSession()
|
||
}
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("shortRegisterLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.registration.lifespan = "100ms"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("longRegisterLifespan", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.flows.registration.lifespan = "1h"
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("browserReturnUrlOry", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.selfservice.allowed_return_urls = [
|
||
"https://www.ory.sh/",
|
||
"https://www.example.org/",
|
||
]
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("remoteCourierRecoveryTemplates", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.courier.templates = {
|
||
recovery: {
|
||
invalid: {
|
||
email: {
|
||
body: {
|
||
html: "base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIGludmFsaWQgcmVjb3ZlcnkgdGVtcGxhdGU=",
|
||
plaintext:
|
||
"base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIGludmFsaWQgcmVjb3ZlcnkgdGVtcGxhdGU=",
|
||
},
|
||
subject: "base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk",
|
||
},
|
||
},
|
||
valid: {
|
||
email: {
|
||
body: {
|
||
html: "base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIHRlbXBsYXRlCnBsZWFzZSByZWNvdmVyIGFjY2VzcyB0byB5b3VyIGFjY291bnQgYnkgY2xpY2tpbmcgdGhlIGZvbGxvd2luZyBsaW5rOgo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4=",
|
||
plaintext:
|
||
"base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIHRlbXBsYXRlCnBsZWFzZSByZWNvdmVyIGFjY2VzcyB0byB5b3VyIGFjY291bnQgYnkgY2xpY2tpbmcgdGhlIGZvbGxvd2luZyBsaW5rOgp7eyAuUmVjb3ZlcnlVUkwgfX0=",
|
||
},
|
||
subject: "base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50",
|
||
},
|
||
},
|
||
},
|
||
}
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("remoteCourierRecoveryCodeTemplates", ({} = {}) => {
|
||
updateConfigFile((config) => {
|
||
config.courier.templates = {
|
||
recovery_code: {
|
||
invalid: {
|
||
email: {
|
||
body: {
|
||
html: "base64://cmVjb3ZlcnlfY29kZV9pbnZhbGlkIFJFTU9URSBURU1QTEFURSBIVE1M", // only
|
||
plaintext:
|
||
"base64://cmVjb3ZlcnlfY29kZV9pbnZhbGlkIFJFTU9URSBURU1QTEFURSBUWFQ=",
|
||
},
|
||
subject:
|
||
"base64://cmVjb3ZlcnlfY29kZV9pbnZhbGlkIFJFTU9URSBURU1QTEFURSBTVUJKRUNU",
|
||
},
|
||
},
|
||
valid: {
|
||
email: {
|
||
body: {
|
||
html: "base://cmVjb3ZlcnlfY29kZV92YWxpZCBSRU1PVEUgVEVNUExBVEUgSFRNTA==",
|
||
plaintext:
|
||
"base64://cmVjb3ZlcnlfY29kZV92YWxpZCBSRU1PVEUgVEVNUExBVEUgVFhU",
|
||
},
|
||
subject:
|
||
"base64://cmVjb3ZlcnlfY29kZV92YWxpZCBSRU1PVEUgVEVNUExBVEUgU1VCSkVDVA==",
|
||
},
|
||
},
|
||
},
|
||
}
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("resetCourierTemplates", (type) => {
|
||
updateConfigFile((config) => {
|
||
if (config?.courier?.templates && type in config.courier.templates) {
|
||
delete config.courier.templates[type]
|
||
}
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"loginOidc",
|
||
({ app, expectSession = true, url = APP_URL + "/login", preTriggerHook }) => {
|
||
cy.visit(url)
|
||
if (preTriggerHook) {
|
||
preTriggerHook()
|
||
}
|
||
cy.triggerOidc(app, "hydra")
|
||
cy.location("href").should("not.eq", "/consent")
|
||
if (expectSession) {
|
||
// for some reason react flakes here although the login succeeded and there should be a session it fails
|
||
if (app === "react") {
|
||
cy.wait(500) // adding arbitrary wait here. not sure if there is a better way in this case
|
||
}
|
||
cy.getSession()
|
||
} else {
|
||
cy.noSession()
|
||
}
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"login",
|
||
({ email, password, expectSession = true, cookieUrl = APP_URL }) => {
|
||
if (expectSession) {
|
||
console.log("Singing in user: ", { email, password })
|
||
} else {
|
||
console.log("Attempting user sign in: ", { email, password })
|
||
}
|
||
|
||
const cookieDomain = new URL(cookieUrl).hostname
|
||
// Somehow, this line makes the clearCookies call work, without it, the cookies are not always cleared and the test fails randomly. Make it make sense.
|
||
cy.getCookies({
|
||
domain: cookieDomain,
|
||
})
|
||
cy.clearCookies({
|
||
domain: cookieDomain,
|
||
})
|
||
|
||
cy.request({
|
||
url: APP_URL + "/self-service/login/browser",
|
||
followRedirect: false,
|
||
failOnStatusCode: false,
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
})
|
||
.then(({ body, status }) => {
|
||
expect(status).to.eq(200)
|
||
const form = body.ui
|
||
return cy.request({
|
||
method: form.method,
|
||
body: mergeFields(form, {
|
||
identifier: email,
|
||
password,
|
||
method: "password",
|
||
}),
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
url: form.action,
|
||
followRedirect: false,
|
||
failOnStatusCode: false,
|
||
})
|
||
})
|
||
.then(({ status, body }) => {
|
||
console.log("Login sequence completed: ", {
|
||
email,
|
||
password,
|
||
expectSession,
|
||
})
|
||
if (expectSession) {
|
||
expect(status).to.eq(200, JSON.stringify(body))
|
||
return cy.getSession()
|
||
} else {
|
||
expect(status).to.not.eq(200, body)
|
||
return cy.noSession()
|
||
}
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("loginMobile", ({ email, password }) => {
|
||
cy.visit(MOBILE_URL + "/Login")
|
||
cy.get('input[data-testid="identifier"]').type(email)
|
||
cy.get('input[data-testid="password"]').type(password)
|
||
cy.get(
|
||
'div[data-testid="field/method/password"] div[data-testid="submit-form"]',
|
||
).click()
|
||
})
|
||
|
||
Cypress.Commands.add("logout", () => {
|
||
cy.getCookies({ domain: "localhost" }).then((cookies) => {
|
||
const c = cookies.find(
|
||
({ name }) => name.indexOf("ory_kratos_session") > -1,
|
||
)
|
||
if (c) {
|
||
cy.clearCookie(c.name, { domain: "localhost" })
|
||
}
|
||
})
|
||
cy.noSession()
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"reauthWithOtherAccount",
|
||
({
|
||
previousUrl,
|
||
expect: { email, success = true },
|
||
type: { email: temail, password: tpassword } = {
|
||
email: undefined,
|
||
password: undefined,
|
||
},
|
||
}) => {
|
||
cy.location("pathname").should("contain", "/login")
|
||
cy.location().then((loc) => {
|
||
const uri = new URLSearchParams(loc.search)
|
||
const flow = uri.get("flow")
|
||
expect(flow).to.not.be.empty
|
||
cy.request({
|
||
url: APP_URL + `/self-service/login/flows?id=${flow}`,
|
||
followRedirect: false,
|
||
failOnStatusCode: false,
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
}).then(({ body, status }) => {
|
||
expect(status).to.eq(200)
|
||
const form = body.ui
|
||
console.log(form.action)
|
||
return cy
|
||
.request({
|
||
method: form.method,
|
||
body: mergeFields(form, {
|
||
identifier: temail || email,
|
||
password: tpassword,
|
||
method: "password",
|
||
}),
|
||
headers: {
|
||
Accept: "application/json",
|
||
ContentType: "application/json",
|
||
},
|
||
url: form.action,
|
||
followRedirect: false,
|
||
failOnStatusCode: false,
|
||
})
|
||
.then((res) => {
|
||
expect(res.status).to.eq(200)
|
||
cy.visit(previousUrl)
|
||
})
|
||
})
|
||
})
|
||
},
|
||
)
|
||
Cypress.Commands.add(
|
||
"reauth",
|
||
({
|
||
expect: { email, success = true },
|
||
type: { email: temail, password: tpassword } = {
|
||
email: undefined,
|
||
password: undefined,
|
||
},
|
||
}) => {
|
||
cy.location("pathname").should("contain", "/login")
|
||
cy.get('input[name="identifier"]').should("have.value", email)
|
||
if (temail) {
|
||
cy.get('input[name="identifier"]').invoke("attr", "value", temail)
|
||
}
|
||
if (tpassword) {
|
||
cy.get('input[name="password"]').clear().type(tpassword)
|
||
}
|
||
cy.longPrivilegedSessionTime()
|
||
cy.get('button[value="password"]').click()
|
||
if (success) {
|
||
cy.location("pathname").should("not.contain", "/login")
|
||
}
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("deleteMail", ({ atLeast = 0 } = {}) => {
|
||
let tries = 0
|
||
let count = 0
|
||
const req = () =>
|
||
cy
|
||
.request("DELETE", `${MAIL_API}/mail`, { pruneCode: "all" })
|
||
.then(({ body }) => {
|
||
count += parseInt(body)
|
||
if (count < atLeast && tries < 100) {
|
||
cy.log(
|
||
`Expected at least ${atLeast} messages but deleteted only ${count} so far (body: ${body})`,
|
||
)
|
||
tries++
|
||
cy.wait(pollInterval)
|
||
return req()
|
||
}
|
||
|
||
return Promise.resolve()
|
||
})
|
||
|
||
try {
|
||
return req()
|
||
} catch (e) {
|
||
cy.log(e)
|
||
// just retry, since retry logic is already implemented in req() and will abort after enough tries
|
||
return req()
|
||
}
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"getSession",
|
||
({ expectAal = "aal1", expectMethods = [], token } = {}) => {
|
||
// Poll /sessions/whoami until it returns 200. cy.request does not retry on
|
||
// its own and chaining .its("status").should("eq", 200) does not re-invoke
|
||
// the request — it only re-evaluates the assertion against the cached
|
||
// response. Browser-driven flows can briefly land in a state where the
|
||
// session cookie has been issued but not yet propagated to Cypress's
|
||
// cookie jar; that's the race that makes the bare cy.request approach
|
||
// flaky on slow runners.
|
||
const buildRequest = () => ({
|
||
method: "GET" as const,
|
||
url: `${KRATOS_PUBLIC}/sessions/whoami`,
|
||
failOnStatusCode: false,
|
||
...(token && {
|
||
auth: {
|
||
bearer: token,
|
||
},
|
||
}),
|
||
})
|
||
|
||
const waitForSession = (
|
||
remaining: number,
|
||
): Cypress.Chainable<Cypress.Response<any>> =>
|
||
cy.request(buildRequest()).then((resp) => {
|
||
if (resp.status === 200) {
|
||
return cy.wrap(resp, { log: false })
|
||
}
|
||
if (remaining <= 0) {
|
||
throw new Error(
|
||
`cy.getSession: /sessions/whoami still ${resp.status} after retries`,
|
||
)
|
||
}
|
||
cy.wait(250, { log: false })
|
||
return waitForSession(remaining - 1)
|
||
})
|
||
|
||
// 20 retries × 250ms = 5s budget for the session cookie to land in
|
||
// Cypress's jar, then assertions on the response.
|
||
return waitForSession(20).then((response) => {
|
||
expect(response.body.id).to.not.be.empty
|
||
expect(dayjs().isBefore(dayjs(response.body.expires_at))).to.be.true
|
||
|
||
// Add a grace second for MySQL which does not support millisecs.
|
||
expect(dayjs().isAfter(dayjs(response.body.issued_at).subtract(1, "s")))
|
||
.to.be.true
|
||
expect(
|
||
dayjs().isAfter(dayjs(response.body.authenticated_at).subtract(1, "s")),
|
||
).to.be.true
|
||
|
||
expect(response.body.identity).to.exist
|
||
|
||
expect(response.body.authenticator_assurance_level).to.equal(expectAal)
|
||
if (expectMethods.length > 0) {
|
||
expect(response.body.authentication_methods).to.have.lengthOf(
|
||
expectMethods.length,
|
||
)
|
||
expectMethods.forEach((value) => {
|
||
expect(
|
||
response.body.authentication_methods.find(
|
||
({ method }) => method === value,
|
||
),
|
||
).to.exist
|
||
})
|
||
}
|
||
|
||
return response.body
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("noSession", () =>
|
||
cy
|
||
.request({
|
||
method: "GET",
|
||
url: `${KRATOS_PUBLIC}/sessions/whoami`,
|
||
failOnStatusCode: false,
|
||
})
|
||
.then((request) => {
|
||
expect(request.status).to.eq(401)
|
||
return request
|
||
}),
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"performEmailVerification",
|
||
({ expect: { email, redirectTo }, strategy = "code" }) => {
|
||
cy.getMail({
|
||
email,
|
||
body: "Verify your account",
|
||
}).then((message) => {
|
||
expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh")
|
||
expect(message.toAddresses).to.have.length(1)
|
||
expect(message.toAddresses[0].trim()).to.equal(email)
|
||
|
||
const link = parseHtml(message.body).querySelector("a")
|
||
expect(link).to.not.be.null
|
||
expect(link.href).to.contain(APP_URL)
|
||
const params = new URL(link.href).searchParams
|
||
|
||
cy.visit(link.href)
|
||
if (strategy === "code") {
|
||
const code = params.get("code")
|
||
expect(code).to.not.be.null
|
||
cy.get(`button[name="method"][value="code"]`).click()
|
||
}
|
||
|
||
if (redirectTo) {
|
||
cy.get(`[data-testid="node/anchor/continue"]`)
|
||
.contains("Continue")
|
||
.click()
|
||
cy.url().should("be.equal", redirectTo)
|
||
}
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"verifyEmail",
|
||
({ expect: { email, password, redirectTo }, strategy }) => {
|
||
cy.performEmailVerification({
|
||
expect: { email, redirectTo },
|
||
strategy,
|
||
}).then(() => {
|
||
cy.getSession().should((session) =>
|
||
assertVerifiableAddress({ email, isVerified: true })(session),
|
||
)
|
||
})
|
||
},
|
||
)
|
||
|
||
// Uses the verification email but waits so that it expires
|
||
Cypress.Commands.add("recoverEmailButExpired", ({ expect: { email } }) => {
|
||
cy.getMail({
|
||
removeMail: true,
|
||
email,
|
||
subject: "Recover access to your account",
|
||
}).then((message) => {
|
||
const link = parseHtml(message.body).querySelector("a")
|
||
expect(link).to.not.be.null
|
||
expect(link.href).to.contain(APP_URL)
|
||
|
||
cy.visit(link.href)
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"recoveryEmailWithCode",
|
||
({ expect: { email, enterCode = true } }) => {
|
||
cy.getMail({
|
||
removeMail: true,
|
||
email,
|
||
body: "Recover access to your account",
|
||
})
|
||
.then((message) => extractOTPCode(message.body))
|
||
.then((code) => {
|
||
expect(code).to.not.be.undefined
|
||
expect(code.length).to.equal(6)
|
||
cy.wrap(code).as("recoveryCode")
|
||
if (enterCode) {
|
||
cy.get("input[name='code']").type(code)
|
||
}
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"recoverEmail",
|
||
({ expect: { email }, shouldVisit = true }) =>
|
||
cy
|
||
.getMail({
|
||
removeMail: true,
|
||
email,
|
||
subject: "Recover access to your account",
|
||
})
|
||
.then((message) => {
|
||
expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh")
|
||
expect(message.toAddresses).to.have.length(1)
|
||
expect(message.toAddresses[0].trim()).to.equal(email)
|
||
|
||
const link = parseHtml(message.body).querySelector("a")
|
||
expect(link).to.not.be.null
|
||
expect(link.href).to.contain(APP_URL)
|
||
|
||
if (shouldVisit) {
|
||
cy.visit(link.href)
|
||
}
|
||
}),
|
||
)
|
||
|
||
// Uses the verification email but waits so that it expires
|
||
Cypress.Commands.add(
|
||
"verifyEmailButExpired",
|
||
({ expect: { email }, strategy = "code" }) => {
|
||
cy.getMail({
|
||
removeMail: true,
|
||
email,
|
||
body: "Verify your account",
|
||
}).then((message) => {
|
||
expect(message.fromAddress.trim()).to.equal("no-reply@ory.kratos.sh")
|
||
expect(message.toAddresses).to.have.length(1)
|
||
expect(message.toAddresses[0].trim()).to.equal(email)
|
||
|
||
const link = parseHtml(message.body).querySelector("a")
|
||
cy.getSession().should((session) => {
|
||
assertVerifiableAddress({
|
||
isVerified: false,
|
||
email: email,
|
||
})(session)
|
||
})
|
||
|
||
cy.visit(link.href)
|
||
if (strategy === "code") {
|
||
cy.get('button[name="method"][value="code"]').click()
|
||
cy.get('[data-testid="ui/message/4070006"]').should(
|
||
"contain.text",
|
||
"The verification code is invalid or has already been used",
|
||
)
|
||
} else {
|
||
cy.get('[data-testid="ui/message/4070005"]').should(
|
||
"contain.text",
|
||
"verification flow expired",
|
||
)
|
||
}
|
||
cy.location("pathname").should("include", "verification")
|
||
|
||
cy.getSession().should((session) => {
|
||
assertVerifiableAddress({
|
||
isVerified: false,
|
||
email: email,
|
||
})(session)
|
||
})
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("useLookupSecrets", (value: boolean) => {
|
||
cy.updateConfigFile((config) => {
|
||
config.selfservice.methods = {
|
||
...config.selfservice.methods,
|
||
lookup_secret: {
|
||
enabled: value,
|
||
},
|
||
}
|
||
return config
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("getLookupSecrets", () =>
|
||
cy
|
||
.get('[data-testid="node/text/lookup_secret_codes/text"] code')
|
||
.then(($e) => $e.map((_, e) => e.innerText.trim()).toArray()),
|
||
)
|
||
Cypress.Commands.add("expectSettingsSaved", () => {
|
||
cy.get('[data-testid="ui/message/1050001"]').should(
|
||
"contain.text",
|
||
"Your changes have been saved",
|
||
)
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"getMail",
|
||
({
|
||
removeMail = true,
|
||
expectedCount = 1,
|
||
email = undefined,
|
||
subject = undefined,
|
||
body = undefined,
|
||
}) => {
|
||
let tries = 0
|
||
const req = () =>
|
||
cy
|
||
.request(`${MAIL_API}/mail`)
|
||
.then((response: Cypress.Response<{ mailItems: MailMessage[] }>) => {
|
||
expect(response.body).to.have.property("mailItems")
|
||
let count = response.body.mailItems.length
|
||
if (count === 0 && tries < 100) {
|
||
tries++
|
||
cy.wait(pollInterval)
|
||
return req()
|
||
}
|
||
|
||
let mailItem: MailMessage
|
||
|
||
if (!subject && !email) {
|
||
expect(count).to.equal(expectedCount)
|
||
mailItem = response.body.mailItems[0]
|
||
} else {
|
||
const filters: ((m: MailMessage) => boolean)[] = []
|
||
if (email) {
|
||
filters.push((m: MailMessage) => m.toAddresses.includes(email))
|
||
}
|
||
if (subject) {
|
||
filters.push((m: MailMessage) => m.subject.includes(subject))
|
||
}
|
||
if (body) {
|
||
filters.push((m: MailMessage) => m.body.includes(body))
|
||
}
|
||
const filtered = response.body.mailItems.filter((m) => {
|
||
return filters.every((f) => f(m))
|
||
})
|
||
|
||
if (filtered.length === 0) {
|
||
tries++
|
||
cy.wait(pollInterval)
|
||
return req()
|
||
}
|
||
|
||
expect(filtered.length).to.equal(expectedCount)
|
||
mailItem = filtered[0]
|
||
|
||
if (removeMail) {
|
||
return cy.deleteMail({ atLeast: count }).then(() => {
|
||
return Promise.resolve(mailItem)
|
||
})
|
||
}
|
||
}
|
||
|
||
return Promise.resolve(mailItem)
|
||
})
|
||
|
||
return req()
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("submitPasswordForm", () => {
|
||
cy.get('[name="method"][value="password"]').click()
|
||
cy.get('[name="method"][value="password"]:disabled').should("not.exist")
|
||
})
|
||
|
||
Cypress.Commands.add("submitProfileForm", (app?: string) => {
|
||
if (app === "mobile") {
|
||
cy.get('[data-testid="field/method/profile"]').click()
|
||
cy.get('[data-testid="field/method/profile"]:disabled').should("not.exist")
|
||
} else {
|
||
cy.get('[name="method"][value="profile"]').click()
|
||
cy.get('[name="method"][value="profile"]:disabled').should("not.exist")
|
||
}
|
||
})
|
||
|
||
Cypress.Commands.add("submitCodeForm", (app) => {
|
||
if (app === "mobile") {
|
||
cy.get('[data-testid="field/method/code"]').click()
|
||
cy.get('[data-testid="field/method/code"]:disabled').should("not.exist")
|
||
} else {
|
||
cy.get('button[name="method"][value="code"]').click()
|
||
cy.get('button[name="method"][value="code"]:disabled').should("not.exist")
|
||
}
|
||
})
|
||
|
||
Cypress.Commands.add("clickWebAuthButton", (type: string) => {
|
||
cy.get('*[data-testid="node/script/webauthn_script"]').should("exist")
|
||
cy.wait(500) // Wait for script to load
|
||
cy.get('*[name="webauthn_' + type + '_trigger"]').click()
|
||
cy.wait(500) // Wait webauth to pass
|
||
})
|
||
|
||
Cypress.Commands.add("shouldShow2FAScreen", () => {
|
||
cy.location().should((loc) => {
|
||
expect(loc.pathname).to.include("/login")
|
||
})
|
||
cy.get("h2")
|
||
.invoke("text")
|
||
.should("match", /Second factor authentication|Two-Factor Authentication/)
|
||
cy.get('[data-testid="ui/message/1010004"]').should(
|
||
"contain.text",
|
||
"Please complete the second authentication challenge.",
|
||
)
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"shouldErrorOnDisallowedReturnTo",
|
||
(init: string, { app }: { app: "express" | "react" }) => {
|
||
cy.visit(init, { failOnStatusCode: false })
|
||
if (app === "react") {
|
||
cy.location("href").should("include", init.split("?")[0])
|
||
cy.get(".Toastify").should(
|
||
"contain.text",
|
||
"The return_to address is not allowed.",
|
||
)
|
||
} else {
|
||
cy.location("pathname").should("contain", "error")
|
||
cy.get("div").should(
|
||
"contain.text",
|
||
'Requested return_to URL "https://not-allowed" is not allowed.',
|
||
)
|
||
}
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"shouldHaveCsrfError",
|
||
({ app }: { app: "express" | "react" }) => {
|
||
let initial: string
|
||
let pathname: string
|
||
cy.location().should((location) => {
|
||
initial = location.search
|
||
pathname = location.pathname
|
||
})
|
||
|
||
cy.getCookies().then((cookies) => {
|
||
const csrf = cookies.find(({ name }) => name.indexOf("csrf") > -1)
|
||
expect(csrf).to.not.be.undefined
|
||
cy.clearCookie(csrf.name)
|
||
})
|
||
cy.submitPasswordForm()
|
||
|
||
// We end up at a new flow
|
||
if (app === "express") {
|
||
cy.location().should((location) => {
|
||
expect(initial).to.not.be.empty
|
||
expect(location.search).to.not.eq(initial)
|
||
})
|
||
|
||
cy.location("pathname").should("include", "/error")
|
||
cy.get(`div`).should("contain.text", "CSRF")
|
||
} else {
|
||
cy.location("pathname").should((got) => {
|
||
expect(got).to.eql(pathname)
|
||
})
|
||
cy.get(".Toastify").should(
|
||
"contain.text",
|
||
"A security violation was detected, please fill out the form again.",
|
||
)
|
||
}
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"triggerOidc",
|
||
(app: "react" | "express", provider: string = "hydra") => {
|
||
let initial, didHaveSearch
|
||
cy.location().then((loc) => {
|
||
didHaveSearch = loc.search.length > 0
|
||
initial = loc.pathname + loc.search
|
||
})
|
||
cy.get('[name="provider"][value="' + provider + '"]').click()
|
||
cy.location().should((loc) => {
|
||
if (app === "express" || didHaveSearch) {
|
||
return
|
||
}
|
||
expect(loc.pathname + loc.search).not.to.eql(initial)
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"removeAttribute",
|
||
(selectors: string[], attribute: string) => {
|
||
selectors.forEach((selector) => {
|
||
cy.get(selector).then(($el) => {
|
||
$el.removeAttr(attribute)
|
||
})
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"addInputElement",
|
||
(parent: string, attribute: string, value: string) => {
|
||
cy.get(parent).then(($el) => {
|
||
$el.append(`<input type="hidden" name="${attribute}" value="${value}" />`)
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add(
|
||
"notifyUnknownRecipients",
|
||
(flow: "recovery" | "verification", value: boolean = true) => {
|
||
cy.updateConfigFile((config) => {
|
||
config.selfservice.flows[flow].notify_unknown_recipients = value
|
||
return config
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("getCourierMessages", () => {
|
||
return cy.request(KRATOS_ADMIN + "/courier/messages").then((res) => {
|
||
return res.body
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add(
|
||
"enableVerificationUIAfterRegistration",
|
||
(strategy: "password" | "oidc" | "webauthn") => {
|
||
cy.updateConfigFile((config) => {
|
||
if (!config.selfservice.flows.registration.after[strategy]) {
|
||
config.selfservice.flows.registration.after = {
|
||
[strategy]: { hooks: [] },
|
||
}
|
||
}
|
||
|
||
const hooks =
|
||
config.selfservice.flows.registration.after[strategy].hooks || []
|
||
config.selfservice.flows.registration.after[strategy].hooks = [
|
||
...hooks.filter((h) => h.hook !== "show_verification_ui"),
|
||
{ hook: "show_verification_ui" },
|
||
]
|
||
return config
|
||
})
|
||
},
|
||
)
|
||
|
||
Cypress.Commands.add("getVerificationCodeFromEmail", (email) => {
|
||
return cy
|
||
.getMail({
|
||
removeMail: true,
|
||
email,
|
||
body: "Verify your account",
|
||
})
|
||
.then((message) => {
|
||
expect(message.toAddresses[0].trim()).to.equal(email)
|
||
const code = extractOTPCode(message.body)
|
||
expect(code).to.not.be.undefined
|
||
expect(code.length).to.equal(6)
|
||
return code
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("getRegistrationCodeFromEmail", (email, opts) => {
|
||
return cy
|
||
.getMail({
|
||
removeMail: true,
|
||
email,
|
||
body: "Complete your account registration with the following code",
|
||
...opts,
|
||
})
|
||
.then((message) => {
|
||
expect(message.toAddresses[0].trim()).to.equal(email)
|
||
})
|
||
.then((message) => {
|
||
const code = extractOTPCode(message.body)
|
||
expect(code).to.not.be.undefined
|
||
expect(code.length).to.equal(6)
|
||
return code
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("getLoginCodeFromEmail", (email, opts) => {
|
||
return cy
|
||
.getMail({
|
||
removeMail: true,
|
||
email,
|
||
body: "Login to your account with the following code",
|
||
...opts,
|
||
})
|
||
.then((message) => {
|
||
expect(message.toAddresses[0].trim()).to.equal(email)
|
||
})
|
||
.then((message) => {
|
||
const code = extractOTPCode(message.body)
|
||
expect(code).to.not.be.undefined
|
||
expect(code.length).to.equal(6)
|
||
return code
|
||
})
|
||
})
|
||
|
||
Cypress.Commands.add("useConfig", (cb) => {
|
||
cy.updateConfigFile((config) => {
|
||
const builder = cb(new ConfigBuilder(config))
|
||
return builder.build()
|
||
})
|
||
})
|