From 0aa5c7438d23e195d15297e6a05d7c1bee87595a Mon Sep 17 00:00:00 2001 From: Cornelius Roemer Date: Mon, 11 Mar 2024 21:29:55 +0100 Subject: [PATCH] fix(website): handle unavailable keycloak service to allow local dev for public pages without keycloak (#1311) * fix(website): handle undefined session Resolves #1163 On most pages, we know users are logged in, otherwise middleware redirects However, middleware apparently doesn't always get a chance to set session, see #1163 * Deal with possibility of keycloak being unavailable * Assert type * Fix format * feat(website): use 503 website when keycloak unavailable * Address review comments and `astro check` warnings --- .../components/Navigation/Navigation.astro | 4 +- website/src/components/User/UserPage.astro | 13 +- .../src/components/common/NeedToLogin.astro | 2 +- website/src/env.d.ts | 2 +- website/src/middleware/authMiddleware.ts | 135 +++++++----------- website/src/pages/503.astro | 25 ++++ website/src/pages/datasets/[datasetId].astro | 5 +- website/src/pages/datasets/index.astro | 6 +- .../src/pages/group/[groupName]/index.astro | 5 +- website/src/utils/KeycloakClientManager.ts | 36 +++++ website/src/utils/clientMetadata.ts | 7 + website/src/utils/getAccessToken.ts | 4 +- website/src/utils/getAuthUrl.ts | 21 +++ website/src/utils/realmPath.ts | 1 + .../src/utils/urlForKeycloakAccountPage.ts | 9 ++ website/tests/e2e.fixture.ts | 4 +- .../pages/sequences/accession.fa.spec.ts | 2 +- .../tests/pages/sequences/accession.spec.ts | 2 +- .../tests/pages/sequences/sequences.page.ts | 2 +- 19 files changed, 178 insertions(+), 107 deletions(-) create mode 100644 website/src/pages/503.astro create mode 100644 website/src/utils/KeycloakClientManager.ts create mode 100644 website/src/utils/clientMetadata.ts create mode 100644 website/src/utils/getAuthUrl.ts create mode 100644 website/src/utils/realmPath.ts create mode 100644 website/src/utils/urlForKeycloakAccountPage.ts diff --git a/website/src/components/Navigation/Navigation.astro b/website/src/components/Navigation/Navigation.astro index 825d7d97b..8ae6d6bbd 100644 --- a/website/src/components/Navigation/Navigation.astro +++ b/website/src/components/Navigation/Navigation.astro @@ -1,12 +1,12 @@ --- import { SandwichMenu } from './SandwichMenu.tsx'; import { cleanOrganism } from './cleanOrganism'; -import { getAuthUrl } from '../../middleware/authMiddleware'; import { navigationItems } from '../../routes'; +import { getAuthUrl } from '../../utils/getAuthUrl'; const { organism, knownOrganisms } = cleanOrganism(Astro.params.organism); -const isLoggedIn = Astro.locals.session.isLoggedIn; +const isLoggedIn = Astro.locals.session?.isLoggedIn ?? false; const loginUrl = await getAuthUrl(Astro.url.toString()); --- diff --git a/website/src/components/User/UserPage.astro b/website/src/components/User/UserPage.astro index 3492e7389..738eee3d7 100644 --- a/website/src/components/User/UserPage.astro +++ b/website/src/components/User/UserPage.astro @@ -2,32 +2,35 @@ import { ListOfGroupsOfUser } from './ListOfGroupsOfUser.tsx'; import { getRuntimeConfig } from '../../config'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getKeycloakClient, urlForKeycloakAccountPage } from '../../middleware/authMiddleware'; import { routes } from '../../routes'; import { GroupManagementClient } from '../../services/groupManagementClient'; +import { KeycloakClientManager } from '../../utils/KeycloakClientManager'; import { getAccessToken } from '../../utils/getAccessToken'; +import { urlForKeycloakAccountPage } from '../../utils/urlForKeycloakAccountPage'; import ErrorBox from '../common/ErrorBox.astro'; import DashiconsGroups from '~icons/dashicons/groups'; import MaterialSymbolsLightPersonOutline from '~icons/material-symbols-light/person-outline'; -const session = Astro.locals.session; +const session = Astro.locals.session!; const user = session.user!; // page only accessible if user is logged in const username = user.username!; // all users must have a username const name = user.name; -const accessToken = getAccessToken(Astro.locals.session)!; +const accessToken = getAccessToken(session)!; const clientConfig = getRuntimeConfig().public; const logoutUrl = new URL(Astro.request.url); logoutUrl.pathname = routes.logout(); -const keycloakLogoutUrl = (await getKeycloakClient()).endSessionUrl({ +const keycloakClient = await KeycloakClientManager.getClient(); + +const keycloakLogoutUrl = keycloakClient!.endSessionUrl({ post_logout_redirect_uri: logoutUrl.href, }); +const accountPageUrl = await urlForKeycloakAccountPage(keycloakClient!); const groupOfUsersResult = await GroupManagementClient.create().getGroupsOfUser(accessToken); -const accountPageUrl = await urlForKeycloakAccountPage(); --- diff --git a/website/src/components/common/NeedToLogin.astro b/website/src/components/common/NeedToLogin.astro index 76e4e063c..453683b9b 100644 --- a/website/src/components/common/NeedToLogin.astro +++ b/website/src/components/common/NeedToLogin.astro @@ -1,5 +1,5 @@ --- -import { getAuthUrl } from '../../middleware/authMiddleware'; +import { getAuthUrl } from '../../utils/getAuthUrl'; import IcOutlineLogin from '~icons/ic/outline-login'; const loginUrl = await getAuthUrl(Astro.url.toString()); diff --git a/website/src/env.d.ts b/website/src/env.d.ts index a04caaa7d..59457281b 100644 --- a/website/src/env.d.ts +++ b/website/src/env.d.ts @@ -18,6 +18,6 @@ type Session = { declare namespace App { interface Locals { - session: Session; + session?: Session; } } diff --git a/website/src/middleware/authMiddleware.ts b/website/src/middleware/authMiddleware.ts index 1a16bacae..4968f6c4d 100644 --- a/website/src/middleware/authMiddleware.ts +++ b/website/src/middleware/authMiddleware.ts @@ -3,11 +3,12 @@ import { defineMiddleware } from 'astro/middleware'; import jsonwebtoken from 'jsonwebtoken'; import JwksRsa from 'jwks-rsa'; import { err, ok, ResultAsync } from 'neverthrow'; -import { type BaseClient, Issuer, type TokenSet } from 'openid-client'; +import { type BaseClient, type TokenSet } from 'openid-client'; -import { getConfiguredOrganisms, getRuntimeConfig } from '../config.ts'; +import { getConfiguredOrganisms } from '../config.ts'; import { getInstanceLogger } from '../logger.ts'; -import { routes } from '../routes'; +import { KeycloakClientManager } from '../utils/KeycloakClientManager.ts'; +import { getAuthUrl } from '../utils/getAuthUrl.ts'; import { shouldMiddlewareEnforceLogin } from '../utils/shouldMiddlewareEnforceLogin.ts'; export const ACCESS_TOKEN_COOKIE = 'access_token'; @@ -19,52 +20,13 @@ enum TokenVerificationError { INVALID_TOKEN, } -export const clientMetadata = { - client_id: 'test-cli', // TODO: #1100 Replace with actual client id - response_types: ['code', 'id_token'], - client_secret: 'someSecret', - public: true, -}; - -export const realmPath = '/realms/loculus'; - -let _keycloakClient: BaseClient | undefined; - const logger = getInstanceLogger('LoginMiddleware'); -export async function getKeycloakClient() { - if (_keycloakClient === undefined) { - const originForClient = getRuntimeConfig().serverSide.keycloakUrl; - - const issuerUrl = `${originForClient}${realmPath}`; - - logger.info(`Getting keycloak client for issuer url: ${issuerUrl}`); - const keycloakIssuer = await Issuer.discover(issuerUrl); - - _keycloakClient = new keycloakIssuer.Client(clientMetadata); - } - - return _keycloakClient; -} - -export const getAuthUrl = async (redirectUrl: string) => { - const logout = routes.logout(); - if (redirectUrl.endsWith(logout)) { - redirectUrl = redirectUrl.replace(logout, routes.userOverviewPage()); - } - const authUrl = (await getKeycloakClient()).authorizationUrl({ - redirect_uri: redirectUrl, - scope: 'openid', - response_type: 'code', - }); - return authUrl; -}; - -async function getValidTokenAndUserInfoFromCookie(context: APIContext) { +async function getValidTokenAndUserInfoFromCookie(context: APIContext, client: BaseClient) { logger.debug(`Trying to get token and user info from cookie`); - const token = await getTokenFromCookie(context); + const token = await getTokenFromCookie(context, client); if (token !== undefined) { - const userInfo = await getUserInfo(token); + const userInfo = await getUserInfo(token, client); if (userInfo.isErr()) { logger.debug(`Cookie token found but could not get user info`); @@ -80,11 +42,11 @@ async function getValidTokenAndUserInfoFromCookie(context: APIContext) { return undefined; } -async function getValidTokenAndUserInfoFromParams(context: APIContext) { +async function getValidTokenAndUserInfoFromParams(context: APIContext, client: BaseClient) { logger.debug(`Trying to get token and user info from params`); - const token = await getTokenFromParams(context); + const token = await getTokenFromParams(context, client); if (token !== undefined) { - const userInfo = await getUserInfo(token); + const userInfo = await getUserInfo(token, client); if (userInfo.isErr()) { logger.debug(`Token found in params but could not get user info`); @@ -100,17 +62,28 @@ async function getValidTokenAndUserInfoFromParams(context: APIContext) { } export const authMiddleware = defineMiddleware(async (context, next) => { - let { token, userInfo } = (await getValidTokenAndUserInfoFromCookie(context)) ?? {}; - if (token === undefined) { - const paramResult = await getValidTokenAndUserInfoFromParams(context); - token = paramResult?.token; - userInfo = paramResult?.userInfo; - - if (token !== undefined) { - logger.debug(`Token found in params, setting cookie`); - setCookie(context, token); - return createRedirectWithModifiableHeaders(removeTokenCodeFromSearchParams(context.url)); + let token: TokenCookie | undefined; + let userInfo; + + const client = await KeycloakClientManager.getClient(); + if (client !== undefined) { + // Only run this when keycloak up + const cookieResult = await getValidTokenAndUserInfoFromCookie(context, client); + token = cookieResult?.token; + userInfo = cookieResult?.userInfo; + if (token === undefined) { + const paramResult = await getValidTokenAndUserInfoFromParams(context, client); + token = paramResult?.token; + userInfo = paramResult?.userInfo; + + if (token !== undefined) { + logger.debug(`Token found in params, setting cookie`); + setCookie(context, token); + return createRedirectWithModifiableHeaders(removeTokenCodeFromSearchParams(context.url)); + } } + } else { + logger.warn(`Keycloak client not available, pretending user logged out`); } const enforceLogin = shouldMiddlewareEnforceLogin( @@ -119,6 +92,10 @@ export const authMiddleware = defineMiddleware(async (context, next) => { ); if (enforceLogin && (userInfo === undefined || userInfo.isErr())) { + if (client === undefined) { + logger.error(`Keycloak client not available, cannot redirect to auth`); + return context.redirect('/503?service=Authentication'); + } return redirectToAuth(context); } @@ -154,7 +131,7 @@ export const authMiddleware = defineMiddleware(async (context, next) => { return next(); }); -async function getTokenFromCookie(context: APIContext) { +async function getTokenFromCookie(context: APIContext, client: BaseClient) { const accessToken = context.cookies.get(ACCESS_TOKEN_COOKIE)?.value; const refreshToken = context.cookies.get(REFRESH_TOKEN_COOKIE)?.value; @@ -166,10 +143,10 @@ async function getTokenFromCookie(context: APIContext) { refreshToken, }; - const verifiedTokenResult = await verifyToken(accessToken); + const verifiedTokenResult = await verifyToken(accessToken, client); if (verifiedTokenResult.isErr() && verifiedTokenResult.error.type === TokenVerificationError.EXPIRED) { logger.debug(`Token expired, trying to refresh`); - return refreshTokenViaKeycloak(tokenCookie); + return refreshTokenViaKeycloak(tokenCookie, client); } if (verifiedTokenResult.isErr()) { logger.info(`Error verifying token: ${verifiedTokenResult.error.message}`); @@ -180,7 +157,7 @@ async function getTokenFromCookie(context: APIContext) { return tokenCookie; } -async function verifyToken(accessToken: string) { +async function verifyToken(accessToken: string, client: BaseClient) { logger.debug(`Verifying token`); const tokenHeader = jsonwebtoken.decode(accessToken, { complete: true })?.header; const kid = tokenHeader?.kid; @@ -191,19 +168,15 @@ async function verifyToken(accessToken: string) { }); } - const keycloakClient = await getKeycloakClient(); - - if (keycloakClient.issuer.metadata.jwks_uri === undefined) { + if (client.issuer.metadata.jwks_uri === undefined) { return err({ type: TokenVerificationError.REQUEST_ERROR, - message: `Keycloak client does not contain jwks_uri: ${JSON.stringify( - keycloakClient.issuer.metadata.jwks_uri, - )}`, + message: `Keycloak client does not contain jwks_uri: ${JSON.stringify(client.issuer.metadata.jwks_uri)}`, }); } const jwksClient = new JwksRsa.JwksClient({ - jwksUri: keycloakClient.issuer.metadata.jwks_uri, + jwksUri: client.issuer.metadata.jwks_uri, }); try { @@ -232,16 +205,14 @@ async function verifyToken(accessToken: string) { } } -async function getUserInfo(token: TokenCookie) { - return ResultAsync.fromPromise((await getKeycloakClient()).userinfo(token.accessToken), (error) => { +async function getUserInfo(token: TokenCookie, client: BaseClient) { + return ResultAsync.fromPromise(client.userinfo(token.accessToken), (error) => { logger.debug(`Error getting user info: ${error}`); return error; }); } -async function getTokenFromParams(context: APIContext): Promise { - const client = await getKeycloakClient(); - +async function getTokenFromParams(context: APIContext, client: BaseClient): Promise { const params = client.callbackParams(context.url.toString()); logger.debug(`Keycloak callback params: ${JSON.stringify(params)}`); if (params.code !== undefined) { @@ -260,7 +231,7 @@ async function getTokenFromParams(context: APIContext): Promise { - const redirect = Response.redirect(url); logger.debug(`Redirecting to ${url}`); + const redirect = Response.redirect(url); return new Response(null, { status: redirect.status, headers: redirect.headers }); }; @@ -314,8 +286,8 @@ function removeTokenCodeFromSearchParams(url: URL): string { return newUrl.toString(); } -async function refreshTokenViaKeycloak(token: TokenCookie): Promise { - const refreshedTokenSet = await (await getKeycloakClient()).refresh(token.refreshToken).catch(() => { +async function refreshTokenViaKeycloak(token: TokenCookie, client: BaseClient): Promise { + const refreshedTokenSet = await client.refresh(token.refreshToken).catch(() => { logger.info(`Failed to refresh token`); return undefined; }); @@ -336,10 +308,3 @@ function extractTokenCookieFromTokenSet(tokenSet: TokenSet | undefined): TokenCo refreshToken, }; } - -export async function urlForKeycloakAccountPage() { - const client = await getKeycloakClient(); - const endsessionUrl = client.endSessionUrl(); - const host = new URL(endsessionUrl).host; - return `https://${host}${realmPath}/account`; -} diff --git a/website/src/pages/503.astro b/website/src/pages/503.astro new file mode 100644 index 000000000..53640d430 --- /dev/null +++ b/website/src/pages/503.astro @@ -0,0 +1,25 @@ +--- +import { capitalCase } from 'change-case'; + +import BaseLayout from '../layouts/BaseLayout.astro'; + +const allowedServiceNames = ['authentication']; + +const serviceParam = Astro.url.searchParams.get('service'); + +let service = 'Internal'; +let bodyService = 'internal service you are trying to access'; +if (serviceParam !== null && serviceParam !== '' && allowedServiceNames.includes(serviceParam.toLowerCase())) { + service = capitalCase(serviceParam); + bodyService = `${service.toLowerCase()} service`; +} +--- + + +
+

{`${service} Service Unavailable`}

+

+ The {bodyService} is currently unavailable. Please check back later. +

+
+
diff --git a/website/src/pages/datasets/[datasetId].astro b/website/src/pages/datasets/[datasetId].astro index a15112165..059e0643d 100644 --- a/website/src/pages/datasets/[datasetId].astro +++ b/website/src/pages/datasets/[datasetId].astro @@ -10,10 +10,11 @@ import type { Dataset } from '../../types/datasetCitation'; import { getAccessToken } from '../../utils/getAccessToken'; const clientConfig = getRuntimeConfig().public; -const accessToken = getAccessToken(Astro.locals.session)!; +const session = Astro.locals.session; +const accessToken = getAccessToken(session)!; const { datasetId = '' } = Astro.params; const version = Astro.url.searchParams.get('version')! || '1'; -const username = Astro.locals.session.user?.username; +const username = session?.user?.username; const datasetClient = DatasetCitationClient.create(); diff --git a/website/src/pages/datasets/index.astro b/website/src/pages/datasets/index.astro index 742bd94ac..adff986bc 100644 --- a/website/src/pages/datasets/index.astro +++ b/website/src/pages/datasets/index.astro @@ -10,9 +10,9 @@ import { DatasetCitationClient } from '../../services/datasetCitationClient.ts'; import { getAccessToken } from '../../utils/getAccessToken'; const clientConfig = getRuntimeConfig().public; -const accessToken = getAccessToken(Astro.locals.session)!; -const username = Astro.locals.session.user!.username!; -const session = Astro.locals.session; +const session = Astro.locals.session!; +const accessToken = getAccessToken(session)!; +const username = session.user!.username!; const datasetClient = DatasetCitationClient.create(); diff --git a/website/src/pages/group/[groupName]/index.astro b/website/src/pages/group/[groupName]/index.astro index 0f5251379..5b4be6ba3 100644 --- a/website/src/pages/group/[groupName]/index.astro +++ b/website/src/pages/group/[groupName]/index.astro @@ -6,8 +6,9 @@ import BaseLayout from '../../../layouts/BaseLayout.astro'; import { GroupManagementClient } from '../../../services/groupManagementClient'; import { getAccessToken } from '../../../utils/getAccessToken'; -const accessToken = getAccessToken(Astro.locals.session)!; -const username = Astro.locals.session.user!.username!; +const session = Astro.locals.session!; +const accessToken = getAccessToken(session)!; +const username = session.user!.username!; const groupName = Astro.params.groupName!; const clientConfig = getRuntimeConfig().public; diff --git a/website/src/utils/KeycloakClientManager.ts b/website/src/utils/KeycloakClientManager.ts new file mode 100644 index 000000000..6c9c82eb9 --- /dev/null +++ b/website/src/utils/KeycloakClientManager.ts @@ -0,0 +1,36 @@ +import { type BaseClient, Issuer } from 'openid-client'; + +import { realmPath } from './realmPath.ts'; +import { getRuntimeConfig } from '../config.ts'; +import { getInstanceLogger } from '../logger.ts'; +import { clientMetadata } from '../utils/clientMetadata.ts'; + +export class KeycloakClientManager { + private static _keycloakClient: BaseClient | undefined; + private static readonly logger = getInstanceLogger('LoginMiddleware'); // Assuming getInstanceLogger is available + + public static async getClient(): Promise { + if (this._keycloakClient !== undefined) { + return this._keycloakClient; + } + + const originForClient = getRuntimeConfig().serverSide.keycloakUrl; + const issuerUrl = `${originForClient}${realmPath}`; + + this.logger.info(`Getting keycloak client for issuer url: ${issuerUrl}`); + + try { + const keycloakIssuer = await Issuer.discover(issuerUrl); + this.logger.info(`Keycloak issuer discovered: ${keycloakIssuer}`); + this._keycloakClient = new keycloakIssuer.Client(clientMetadata); + } catch (error: any) { + if (error.code !== 'ECONNREFUSED') { + this.logger.error(`Error discovering keycloak issuer: ${error}`); + throw error; + } + this.logger.warn(`Connection refused when trying to discover the keycloak issuer at url: ${issuerUrl}`); + } + + return this._keycloakClient; + } +} diff --git a/website/src/utils/clientMetadata.ts b/website/src/utils/clientMetadata.ts new file mode 100644 index 000000000..d623de7de --- /dev/null +++ b/website/src/utils/clientMetadata.ts @@ -0,0 +1,7 @@ +// TODO: #1337 Move to config +export const clientMetadata = { + client_id: 'test-cli', // TODO: #1100 Replace with actual client id + response_types: ['code', 'id_token'], + client_secret: 'someSecret', + public: true, +}; diff --git a/website/src/utils/getAccessToken.ts b/website/src/utils/getAccessToken.ts index d21742ab0..b9bb74501 100644 --- a/website/src/utils/getAccessToken.ts +++ b/website/src/utils/getAccessToken.ts @@ -1,3 +1,3 @@ -export function getAccessToken(session: Session) { - return session.token?.accessToken; +export function getAccessToken(session: Session | undefined): string | undefined { + return session?.token?.accessToken; } diff --git a/website/src/utils/getAuthUrl.ts b/website/src/utils/getAuthUrl.ts new file mode 100644 index 000000000..8277ec050 --- /dev/null +++ b/website/src/utils/getAuthUrl.ts @@ -0,0 +1,21 @@ +import { KeycloakClientManager } from './KeycloakClientManager'; +import { routes } from '../routes'; + +export const getAuthUrl = async (redirectUrl: string) => { + const logout = routes.logout(); + if (redirectUrl.endsWith(logout)) { + redirectUrl = redirectUrl.replace(logout, routes.userOverviewPage()); + } + + // Beware: relative url does not work with Redirect.response() + const client = await KeycloakClientManager.getClient(); + if (client === undefined) { + return `/503?service=Authentication`; + } + const authUrl = client.authorizationUrl({ + redirect_uri: redirectUrl, + scope: 'openid', + response_type: 'code', + }); + return authUrl; +}; diff --git a/website/src/utils/realmPath.ts b/website/src/utils/realmPath.ts new file mode 100644 index 000000000..8c8fc07c7 --- /dev/null +++ b/website/src/utils/realmPath.ts @@ -0,0 +1 @@ +export const realmPath = '/realms/loculus'; // TODO: #1339 Move realm path to config diff --git a/website/src/utils/urlForKeycloakAccountPage.ts b/website/src/utils/urlForKeycloakAccountPage.ts new file mode 100644 index 000000000..be8ed7eb3 --- /dev/null +++ b/website/src/utils/urlForKeycloakAccountPage.ts @@ -0,0 +1,9 @@ +import { type BaseClient } from 'openid-client'; + +import { realmPath } from './realmPath.ts'; + +export async function urlForKeycloakAccountPage(client: BaseClient) { + const endsessionUrl = client.endSessionUrl(); + const host = new URL(endsessionUrl).host; + return `https://${host}${realmPath}/account`; +} diff --git a/website/tests/e2e.fixture.ts b/website/tests/e2e.fixture.ts index 14d6cc84d..5015d3061 100644 --- a/website/tests/e2e.fixture.ts +++ b/website/tests/e2e.fixture.ts @@ -17,11 +17,13 @@ import { SubmitPage } from './pages/submission/submit.page'; import { GroupPage } from './pages/user/group/group.page.ts'; import { UserPage } from './pages/user/userPage/userPage.ts'; import { createGroup } from './util/backendCalls.ts'; -import { ACCESS_TOKEN_COOKIE, clientMetadata, realmPath, REFRESH_TOKEN_COOKIE } from '../src/middleware/authMiddleware'; +import { ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE } from '../src/middleware/authMiddleware'; import { BackendClient } from '../src/services/backendClient'; import { groupManagementApi } from '../src/services/groupManagementApi.ts'; import { GroupManagementClient } from '../src/services/groupManagementClient.ts'; import { type DataUseTerms, type Group, openDataUseTermsType } from '../src/types/backend.ts'; +import { clientMetadata } from '../src/utils/clientMetadata.ts'; +import { realmPath } from '../src/utils/realmPath.ts'; type E2EFixture = { searchPage: SearchPage; diff --git a/website/tests/pages/sequences/accession.fa.spec.ts b/website/tests/pages/sequences/accession.fa.spec.ts index 933369070..602fb0b91 100644 --- a/website/tests/pages/sequences/accession.fa.spec.ts +++ b/website/tests/pages/sequences/accession.fa.spec.ts @@ -1,6 +1,6 @@ import { routes } from '../../../src/routes.ts'; -import { baseUrl, dummyOrganism, expect, test, testSequenceEntryData } from '../../e2e.fixture'; import { getAccessionVersionString } from '../../../src/utils/extractAccessionVersion.ts'; +import { baseUrl, expect, test, testSequenceEntryData } from '../../e2e.fixture'; import { getTestSequences } from '../../util/testSequenceProvider.ts'; test.describe('The sequence.fa page', () => { diff --git a/website/tests/pages/sequences/accession.spec.ts b/website/tests/pages/sequences/accession.spec.ts index bd2f489c2..86892df00 100644 --- a/website/tests/pages/sequences/accession.spec.ts +++ b/website/tests/pages/sequences/accession.spec.ts @@ -1,6 +1,6 @@ import { routes } from '../../../src/routes.ts'; import { getAccessionVersionString } from '../../../src/utils/extractAccessionVersion.ts'; -import { baseUrl, dummyOrganism, expect, test, testSequenceEntryData } from '../../e2e.fixture'; +import { baseUrl, expect, test, testSequenceEntryData } from '../../e2e.fixture'; import { getTestSequences } from '../../util/testSequenceProvider.ts'; test.describe('The detailed sequence page', () => { diff --git a/website/tests/pages/sequences/sequences.page.ts b/website/tests/pages/sequences/sequences.page.ts index 3d0e4f6eb..7255841ae 100644 --- a/website/tests/pages/sequences/sequences.page.ts +++ b/website/tests/pages/sequences/sequences.page.ts @@ -3,7 +3,7 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { routes } from '../../../src/routes.ts'; import type { AccessionVersion } from '../../../src/types/backend.ts'; import { getAccessionVersionString } from '../../../src/utils/extractAccessionVersion.ts'; -import { baseUrl, dummyOrganism } from '../../e2e.fixture'; +import { baseUrl } from '../../e2e.fixture'; export class SequencePage { public readonly notLatestVersionBanner: Locator;