diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 703b48c58aa..cac00bde39a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,39 @@ name: Quality -on: pull_request +on: + pull_request: + schedule: + # Every morning at 6:00 AM CET + - cron: '0 4 * * *' + workflow_dispatch: + inputs: + target-env: + description: 'Zitadel target environment to run the acceptance tests against.' + required: true + type: choice + options: + - 'qa' + - 'prod' jobs: + matrix: + # If the workflow is triggered by a schedule event, only the acceptance tests run against QA and Prod. + name: Matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - name: Matrix + id: matrix + run: | + if [ -n "${{ github.event.schedule }}" ]; then + echo 'matrix=["test:acceptance:qa", "test:acceptance:prod"]' >> $GITHUB_OUTPUT + elif [ -n "${{ github.event.inputs.target-env }}" ]; then + echo 'matrix=["test:acceptance:${{ github.event.inputs.target-env }}"]' >> $GITHUB_OUTPUT + else + echo 'matrix=["format --check", "lint", "test:unit", "test:integration", "test:acceptance"]' >> $GITHUB_OUTPUT + fi + quality: name: Ensure Quality @@ -13,15 +44,13 @@ jobs: permissions: contents: "read" + needs: + - matrix + strategy: fail-fast: false matrix: - command: - - format --check - - lint - - test:unit - - test:integration - - test:acceptance + command: ${{ fromJson( needs.matrix.outputs.matrix ) }} steps: - name: Checkout Repo @@ -55,7 +84,7 @@ jobs: # We can cache the Playwright binary independently from the pnpm cache, because we install it separately. # After pnpm install --frozen-lockfile, we can get the version so we only have to download the binary once per version. - run: echo "PLAYWRIGHT_VERSION=$(npx playwright --version | cut -d ' ' -f 2)" >> $GITHUB_ENV - if: ${{ matrix.command == 'test:acceptance' }} + if: ${{ startsWith(matrix.command, 'test:acceptance') }} - uses: actions/cache@v4.0.2 name: Setup Playwright binary cache @@ -65,11 +94,11 @@ jobs: key: ${{ runner.os }}-playwright-binary-${{ env.PLAYWRIGHT_VERSION }} restore-keys: | ${{ runner.os }}-playwright-binary- - if: ${{ matrix.command == 'test:acceptance' }} + if: ${{ startsWith(matrix.command, 'test:acceptance') }} - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps - if: ${{ matrix.command == 'test:acceptance' && steps.playwright-cache.outputs.cache-hit != 'true' }} + if: ${{ startsWith(matrix.command, 'test:acceptance') && steps.playwright-cache.outputs.cache-hit != 'true' }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -79,10 +108,19 @@ jobs: run: ZITADEL_DEV_UID=root pnpm run-sink if: ${{ matrix.command == 'test:acceptance' }} + - name: Create Cloud Env File + run: | + if [ "${{ matrix.command }}" == "test:acceptance:prod" ]; then + echo "${{ secrets.ENV_FILE_CONTENT_ACCEPTANCE_PROD }}" | tee apps/login/.env.local acceptance/tests/.env.local > /dev/null + else + echo "${{ secrets.ENV_FILE_CONTENT_ACCEPTANCE_QA }}" | tee apps/login/.env.local acceptance/tests/.env.local > /dev/null + fi + if: ${{ matrix.command == 'test:acceptance:qa' || matrix.command == 'test:acceptance:prod' }} + - name: Create Production Build run: pnpm build - if: ${{ matrix.command == 'test:acceptance' }} + if: ${{ startsWith(matrix.command, 'test:acceptance') }} - name: Check id: check - run: pnpm ${{ matrix.command }} + run: pnpm ${{ contains(matrix.command, 'test:acceptance') && 'test:acceptance' || matrix.command }} diff --git a/acceptance/docker-compose.yaml b/acceptance/docker-compose.yaml index b02651745e6..de6990387df 100644 --- a/acceptance/docker-compose.yaml +++ b/acceptance/docker-compose.yaml @@ -1,7 +1,7 @@ services: zitadel: user: "${ZITADEL_DEV_UID}" - image: ghcr.io/zitadel/zitadel:v2.65.0 + image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.65.0}" command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml' ports: - "8080:8080" diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx index 49ca7531c13..2d67299c9c7 100644 --- a/apps/login/src/components/login-otp.tsx +++ b/apps/login/src/components/login-otp.tsx @@ -84,7 +84,7 @@ export function LoginOTP({ value: host ? { urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}&organization={{.OrgID}}` + + `${host.includes("localhost") ? "http://" : "https://"}${host}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}` + (authRequestId ? `&authRequestId=${authRequestId}` : ""), } : {}, @@ -107,14 +107,19 @@ export function LoginOTP({ challenges, authRequestId, }) - .catch((error) => { - setError(error.message ?? "Could not request OTP challenge"); + .catch(() => { + setError("Could not request OTP challenge"); return; }) .finally(() => { setLoading(false); }); + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + return response; } @@ -167,6 +172,11 @@ export function LoginOTP({ setLoading(false); }); + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + return response; } diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx index d72749e7572..2f1cd53363c 100644 --- a/apps/login/src/components/login-passkey.tsx +++ b/apps/login/src/components/login-passkey.tsx @@ -110,6 +110,11 @@ export function LoginPasskey({ setLoading(false); }); + if (session && "error" in session && session.error) { + setError(session.error); + return; + } + return session; } @@ -132,6 +137,11 @@ export function LoginPasskey({ setLoading(false); }); + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + return response; } diff --git a/apps/login/src/lib/cookies.ts b/apps/login/src/lib/cookies.ts index 00540653505..4d29b9e7d45 100644 --- a/apps/login/src/lib/cookies.ts +++ b/apps/login/src/lib/cookies.ts @@ -142,7 +142,7 @@ export async function removeSessionFromCookie( } } -export async function getMostRecentSessionCookie(): Promise { +export async function getMostRecentSessionCookie(): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 9726ce84e42..6c2b6cebe63 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -132,21 +132,23 @@ export async function updateSession(options: UpdateSessionCommand) { challenges, } = options; const recentSession = sessionId - ? await getSessionCookieById({ sessionId }).catch((error) => { - return Promise.reject(error); - }) + ? await getSessionCookieById({ sessionId }) : loginName - ? await getSessionCookieByLoginName({ loginName, organization }).catch( - (error) => { - return Promise.reject(error); - }, - ) - : await getMostRecentSessionCookie().catch((error) => { - return Promise.reject(error); - }); + ? await getSessionCookieByLoginName({ loginName, organization }) + : await getMostRecentSessionCookie(); + + if (!recentSession) { + return { + error: "Could not find session", + }; + } const host = (await headers()).get("host"); + if (!host) { + return { error: "Could not get host" }; + } + if ( host && challenges && @@ -174,6 +176,10 @@ export async function updateSession(options: UpdateSessionCommand) { lifetime, ); + if (!session) { + return { error: "Could not update session" }; + } + // if password, check if user has MFA methods let authMethods; if (checks && checks.password && session.factors?.user?.id) { diff --git a/playwright.config.ts b/playwright.config.ts index 0ca27fe1ed7..342a302461c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -72,6 +72,7 @@ export default defineConfig({ ], /* Run local dev server before starting the tests */ + webServer: { command: "pnpm start:built", url: "http://127.0.0.1:3000",