diff --git a/roles/sys-ctl-hlth-msmtp/tasks/00_core.yml b/roles/sys-ctl-hlth-msmtp/tasks/00_core.yml index e7d1a3f458..96c5f28c62 100644 --- a/roles/sys-ctl-hlth-msmtp/tasks/00_core.yml +++ b/roles/sys-ctl-hlth-msmtp/tasks/00_core.yml @@ -9,7 +9,6 @@ - "MAIL_RECIPIENT={{ lookup('mail_recipient') }}" - "MAIL_TIMEOUT={{ lookup('email').timeout }}" - system_service_state: "{{ omit if (service_loader_preloading | default(false) | bool) else SYS_SERVICE_DEFAULT_STATE }}" - system_service_force_flush_instant: "{{ (MODE_DEBUG | bool) and not (service_loader_preloading | default(false) | bool) }}" + system_service_force_flush_instant: "{{ MODE_DEBUG | bool }}" system_service_force_flush_final: false - include_tasks: utils/once/flag.yml diff --git a/roles/test-e2e-playwright/files/personas/utils/env.js b/roles/test-e2e-playwright/files/personas/utils/env.js index 2afb5609b7..0eea54eb46 100644 --- a/roles/test-e2e-playwright/files/personas/utils/env.js +++ b/roles/test-e2e-playwright/files/personas/utils/env.js @@ -132,6 +132,29 @@ function safeIsEnabled(name) { } } +/** + * Video-recording options for a hand-built browser context. + * + * Playwright applies the project's `use.video` setting only to the context the + * `page` fixture provides, so a context from `browser.newContext()` records + * nothing and its spec reaches the artefact with a trace but no video. Spread + * the result into the `newContext` options to follow the project setting. + * + * Args: + * testInfo: the spec's TestInfo; supplies the per-test output directory and + * the resolved project config. + * + * Returns: + * `{ recordVideo: { dir } }` when the project asks for video, else `{}`. + */ +function recordVideoOptions(testInfo) { + const configured = testInfo?.project?.use?.video; + const mode = + configured && typeof configured === "object" ? configured.mode : configured; + if (!mode || mode === "off") return {}; + return { recordVideo: { dir: testInfo.outputDir } }; +} + module.exports = { decodeDotenvQuoted, normalizeUrl, @@ -141,4 +164,5 @@ module.exports = { apiGetOnion, safeSkipUnlessEnabled, safeIsEnabled, + recordVideoOptions, }; diff --git a/roles/web-app-stalwart/files/playwright/test-mailflow.js b/roles/web-app-stalwart/files/playwright/test-mailflow.js index d64494ecc5..9477c26ae9 100644 --- a/roles/web-app-stalwart/files/playwright/test-mailflow.js +++ b/roles/web-app-stalwart/files/playwright/test-mailflow.js @@ -1,6 +1,6 @@ const { test, expect } = require("@playwright/test"); -const { safeSkipUnlessEnabled, gotoOnion } = require("./personas"); +const { safeSkipUnlessEnabled, gotoOnion, recordVideoOptions } = require("./personas"); const { roundcubeSsoLogin, roundcubeLogout, waitForEmailInMailbox } = require("./webmail"); const { webmailBaseUrl, @@ -51,7 +51,7 @@ async function leaveOpenDialogs(page) { // answer. Login is via Keycloak SSO (Roundcube XOAUTH2 -> Stalwart), mirroring // web-app-mailu. They are separate people: isolated browser contexts, and biber stays // signed in so the answer lands in a session that never saw the outbound message. -test("stalwart: biber and the administrator exchange mail both ways", async ({ browser }) => { +test("stalwart: biber and the administrator exchange mail both ways", async ({ browser }, testInfo) => { test.skip(isSplitRealmOidc(), "clearnet app with an onion OIDC issuer: unreachable from one browser"); safeSkipUnlessEnabled("sso"); expect(webmailBaseUrl, "WEBMAIL_BASE_URL must be set").toBeTruthy(); @@ -66,6 +66,7 @@ test("stalwart: biber and the administrator exchange mail both ways", async ({ b const contextOptions = { ignoreHTTPSErrors: true, ...(proxyServer ? { proxy: { server: proxyServer } } : {}), + ...recordVideoOptions(testInfo), }; const biberContext = await browser.newContext(contextOptions); const adminContext = await browser.newContext(contextOptions); diff --git a/roles/web-app-stalwart/files/playwright/test-onion-mailflow.js b/roles/web-app-stalwart/files/playwright/test-onion-mailflow.js index c873433bca..dd5fae64d7 100644 --- a/roles/web-app-stalwart/files/playwright/test-onion-mailflow.js +++ b/roles/web-app-stalwart/files/playwright/test-onion-mailflow.js @@ -1,6 +1,6 @@ const { test, expect } = require("@playwright/test"); -const { safeSkipUnlessEnabled, gotoOnion } = require("./personas"); +const { safeSkipUnlessEnabled, gotoOnion, recordVideoOptions } = require("./personas"); const { roundcubeSsoLogin, roundcubeLogout } = require("./webmail"); const { webmailBaseUrl, biberUsername, biberPassword } = require("./env"); const { resolveTimeout, isSplitRealmOidc } = require("./timeouts"); @@ -17,7 +17,7 @@ const { resolveTimeout, isSplitRealmOidc } = require("./timeouts"); // and never reach the gateway. const FOREIGN_ONION = "recipient@nexusprobe7xk3mjqzvhbnd4rlyugc2pfe6sotai5w.onion"; -test("biber: a .onion recipient is accepted and routed through the Tor gateway", async ({ browser }) => { +test("biber: a .onion recipient is accepted and routed through the Tor gateway", async ({ browser }, testInfo) => { test.skip(isSplitRealmOidc(), "clearnet app with an onion OIDC issuer: unreachable from one browser"); safeSkipUnlessEnabled("tor"); safeSkipUnlessEnabled("sso"); @@ -29,6 +29,7 @@ test("biber: a .onion recipient is accepted and routed through the Tor gateway", const context = await browser.newContext({ ignoreHTTPSErrors: true, ...(proxyServer ? { proxy: { server: proxyServer } } : {}), + ...recordVideoOptions(testInfo), }); try { diff --git a/roles/web-app-stalwart/tasks/jmap_call.yml b/roles/web-app-stalwart/tasks/jmap_call.yml index 658771112f..90d6f13c05 100644 --- a/roles/web-app-stalwart/tasks/jmap_call.yml +++ b/roles/web-app-stalwart/tasks/jmap_call.yml @@ -12,7 +12,13 @@ changed_when: false retries: 12 delay: 5 - until: stalwart_jmap_raw.rc == 0 + until: >- + stalwart_jmap_raw.rc == 0 + and ( + (stalwart_jmap_raw.stdout | from_json).methodResponses[0][0] != 'error' + or (stalwart_jmap_raw.stdout | from_json).methodResponses[0][1].type + not in STALWART_JMAP_TRANSIENT_ERRORS + ) timeout: "{{ lookup('timeout', 120) | int }}" # nocheck: include-uniform-apply-keyword — seven call sites include this file; the in-file no_log is the SPOT, per-site apply would repeat it 7× no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" diff --git a/roles/web-app-stalwart/vars/main.yml b/roles/web-app-stalwart/vars/main.yml index 61ffa93d79..9aece8fdec 100644 --- a/roles/web-app-stalwart/vars/main.yml +++ b/roles/web-app-stalwart/vars/main.yml @@ -15,6 +15,9 @@ STALWART_LOG_DIR: "{{ lookup('volume', application_id, 'data').target }}" STALWART_JMAP_APPLICATION_GET: >- {{ {'using': ['urn:ietf:params:jmap:core', 'urn:stalwart:jmap'], 'methodCalls': [['x:Application/get', {'ids': none}, 'c0']]} | to_json }} +STALWART_JMAP_TRANSIENT_ERRORS: + - serverUnavailable + - rateLimit STALWART_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, 'stalwart') }}" STALWART_DOMAIN: "{{ lookup('config', application_id, 'services.stalwart.domain') }}"