78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// What a failed sign-in becomes (spec §7): a redirect to the sign-in page with a code from a fixed
|
|
// set, never the provider's text, and never to an address outside this app.
|
|
|
|
import { forbidden, unauthenticated } from "@conusai-kit/core"
|
|
import { describe, expect, it } from "vitest"
|
|
import {
|
|
AUTH_ERROR_MESSAGES,
|
|
AUTH_ERRORS,
|
|
authErrorOf,
|
|
parseAuthError,
|
|
safeReturnTo,
|
|
signInErrorHref,
|
|
} from "../apps/web/src/server/auth-errors.js"
|
|
import { appCatalogues } from "../apps/web/src/ui/messages.js"
|
|
|
|
const url = (query = ""): URL => new URL(`https://app.example.test/auth/callback${query}`)
|
|
|
|
describe("which code a failure gets", () => {
|
|
it("is `denied` when the provider said no, whatever the provider's text", () => {
|
|
expect(authErrorOf(new Error("anything"), url("?error=access_denied&error_description=secret+words"))).toBe(
|
|
"denied",
|
|
)
|
|
})
|
|
|
|
it("is `noTransaction` for a callback no sign-in started in this browser", () => {
|
|
const refusal = unauthenticated("no sign-in is in progress for this browser; start again at /auth/sign-in")
|
|
expect(authErrorOf(refusal, url())).toBe("noTransaction")
|
|
})
|
|
|
|
it("is `sessionExpired` for any other 401, `failed` for any other refusal of the app's own", () => {
|
|
expect(authErrorOf(unauthenticated("the session ended"), url())).toBe("sessionExpired")
|
|
expect(authErrorOf(forbidden("no"), url())).toBe("failed")
|
|
})
|
|
|
|
it("is `failed` for a deployment that has no identity provider to complete a sign-in with", () => {
|
|
expect(authErrorOf(unauthenticated("this deployment has no identity provider configured"), url())).toBe("failed")
|
|
})
|
|
|
|
it("is `provider` for what is not the app's own error: the exchange with the provider failed", () => {
|
|
expect(authErrorOf(new TypeError("fetch failed"), url())).toBe("provider")
|
|
})
|
|
})
|
|
|
|
describe("the sign-in page's address", () => {
|
|
it("carries the code and, when it is safe, where the person was going", () => {
|
|
expect(signInErrorHref("denied")).toBe("/auth/sign-in?error=denied")
|
|
expect(signInErrorHref("failed", "/invitations/acme/abc")).toBe(
|
|
"/auth/sign-in?error=failed&return_to=%2Finvitations%2Facme%2Fabc",
|
|
)
|
|
})
|
|
|
|
it.each(["https://evil.example", "//evil.example", "/\\evil.example", "javascript:alert(1)", "", null, undefined])(
|
|
"never returns to %j",
|
|
(target) => {
|
|
expect(safeReturnTo(target)).toBe("/")
|
|
expect(signInErrorHref("failed", target)).toBe("/auth/sign-in?error=failed")
|
|
},
|
|
)
|
|
|
|
it("shows only the five words, not whatever the query holds", () => {
|
|
expect(parseAuthError("denied")).toBe("denied")
|
|
expect(parseAuthError("<script>alert(1)</script>")).toBeUndefined()
|
|
expect(parseAuthError(undefined)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("the words for each code", () => {
|
|
it("exist in English and in Lithuanian for every code", () => {
|
|
for (const code of AUTH_ERRORS) {
|
|
const [module, ...rest] = (AUTH_ERROR_MESSAGES[code] as string).split(".")
|
|
const key = rest.join(".")
|
|
expect(appCatalogues.en?.[module as string]?.[key], `en ${code}`).toBeTruthy()
|
|
expect(appCatalogues.lt?.[module as string]?.[key], `lt ${code}`).toBeTruthy()
|
|
}
|
|
})
|
|
})
|