Zum Inhalt springen
  1. Ein Skript deckt native iOS- und Android-, React Native- und Flutter-Apps ab, weil Maestro die App von außen steuert. Speichere es neben deinen Abläufen als proxymail.js:

    proxymail.js
    // Proxy Mail QA for Maestro flows: works for native iOS and Android, React
    // Native and Flutter apps. Guide: https://proxymail.qa/docs/maestro
    //
    // - runScript: { file: proxymail.js, env: { ACTION: create } }
    // output.address, output.password, output.since. Add ENVIRONMENT
    // (staging), AGE (adult, under18…) or COUNTRY (GB) to set them.
    // - runScript: { file: proxymail.js, env: { ACTION: code } } (or link)
    // output.code (or output.link), for mail received since output.since.
    // Add APP_LINK: true for the link that opens the app (myapp://…) when
    // the email also has a web one, UNUSED: true to skip a used code, and
    // TYPE: coupon for a coupon or promo code (any for either), or for a
    // link, TYPE: unsubscribe for the email's unsubscribe link.
    // - runScript: { file: proxymail.js, env: { ACTION: count } }
    // output.count, the emails received since output.since
    // - runScript: { file: proxymail.js, env: { ACTION: check, EXPECT_HOST: links.example.com } }
    // fails the flow if the newest email fails a check (SPF, DKIM, DMARC,
    // local or http links, where its link goes…); output.failed lists them
    // - runScript: { file: proxymail.js, env: { ACTION: none } }
    // fails the flow if an email arrives within 30 seconds (TIMEOUT);
    // CATEGORY: marketing watches only for offers and newsletters
    // - runScript: { file: proxymail.js, env: { ACTION: signin, TAGS: verified } }
    // output.address and output.password of a saved test account; needs a
    // key allowed to read saved passwords
    // - runScript: { file: proxymail.js, env: { ACTION: checkout, TAGS: verified } }
    // borrows an account no other test has: output.address, output.password,
    // output.token. ACTION: return gives it back (ADD_TAGS, REMOVE_TAGS).
    // - runScript: { file: proxymail.js, env: { ACTION: coupon_result, RESULT: accepted } }
    // after applying the coupon at checkout (the store's sandbox, never a
    // real card): RESULT is accepted, rejected or error; add MESSAGE (what
    // the app said), CODE and ONE_TIME: true. output.verdict; fails the
    // flow if the coupon_result check fails.
    // - runScript: { file: proxymail.js, env: { ACTION: consent, MARKETING: declined } }
    // records what the account agreed to at sign-up: MARKETING opted_in or
    // declined; add PRECHECKED: true when the box started ticked and
    // DOUBLE_OPT_IN: pending or confirmed.
    //
    // The API key comes from the run: maestro test -e PROXYMAIL_QA_API_KEY=... flow.yaml
    // Add -e PROXYMAIL_QA_RUN=$PROXYMAIL_QA_RUN to add the calls to a run (the
    // GitHub Action sets it), and TEST: a name, in env, to name the step.
    const API = 'https://api.proxymail.qa/v1';
    const headers = { Authorization: 'Bearer ' + PROXYMAIL_QA_API_KEY, 'Content-Type': 'application/json' };
    if (typeof PROXYMAIL_QA_RUN !== 'undefined' && PROXYMAIL_QA_RUN) {
    headers['Proxy-Mail-QA-Run'] = PROXYMAIL_QA_RUN;
    // Percent-encoded: a test's name can hold characters headers can't.
    if (typeof TEST !== 'undefined' && TEST) headers['Proxy-Mail-QA-Test'] = encodeURIComponent(TEST);
    }
    function call(method, path, body, extra) {
    const options = { headers: extra || headers };
    if (body) options.body = JSON.stringify(body);
    const response = method === 'POST' ? http.post(API + path, options) : method === 'PUT' ? http.put(API + path, options) : http.get(API + path, options);
    return { ok: response.ok, status: response.status, body: response.body ? json(response.body) : {} };
    }
    function fail(response) {
    // When nothing arrives, the hint says why (an email provider in sandbox mode, say).
    throw new Error('Proxy Mail QA: ' + (response.body.message || response.status) + ' ' + (response.body.hint || ''));
    }
    function tags(value) {
    return String(value).split(',').map(function (tag) { return tag.trim(); }).filter(function (tag) { return tag; });
    }
    function account(body) {
    output.address = body.address;
    output.password = body.password;
    output.since = new Date().toISOString();
    }
    const at = typeof output.address === 'undefined' ? '' : '/addresses/' + encodeURIComponent(output.address) + '/';
    const since = '?since=' + encodeURIComponent(output.since);
    if (ACTION === 'create') {
    const body = { password: true, persona: true };
    if (typeof ENVIRONMENT !== 'undefined') body.environment = ENVIRONMENT;
    if (typeof AGE !== 'undefined') body.age = AGE;
    if (typeof COUNTRY !== 'undefined') body.country = COUNTRY;
    const created = call('POST', '/addresses', body);
    if (!created.ok) fail(created);
    account(created.body);
    } else if (ACTION === 'count') {
    const counted = call('GET', at + 'count' + since);
    if (!counted.ok) fail(counted);
    output.count = counted.body.count;
    } else if (ACTION === 'check') {
    const host = typeof EXPECT_HOST !== 'undefined' ? '&expect_host=' + encodeURIComponent(EXPECT_HOST) : '';
    const checked = call('GET', at + 'checks' + since + host);
    if (!checked.ok) fail(checked);
    output.failed = checked.body.failed.join(', ');
    const failures = checked.body.checks.filter(function (c) { return c.status === 'fail'; }).map(function (c) { return c.check + ': ' + c.detail; });
    if (failures.length) throw new Error('Proxy Mail QA: the email failed ' + failures.join(' '));
    } else if (ACTION === 'none') {
    const timeout = typeof TIMEOUT !== 'undefined' ? TIMEOUT : 30;
    const category = typeof CATEGORY !== 'undefined' ? '&category=' + encodeURIComponent(CATEGORY) : '';
    const watched = call('GET', at + 'none' + since + '&timeout=' + timeout + category);
    if (!watched.ok) fail(watched);
    } else if (ACTION === 'signin') {
    const body = { tags: typeof TAGS !== 'undefined' ? tags(TAGS) : [] };
    if (typeof ENVIRONMENT !== 'undefined') body.environment = ENVIRONMENT;
    const found = call('POST', '/addresses/sign-in', body);
    if (!found.ok) fail(found);
    account(found.body);
    } else if (ACTION === 'checkout') {
    const held = call('POST', '/addresses/checkout', { tags: tags(TAGS) });
    if (!held.ok) fail(held);
    // Its saved password, when the key may read it.
    const secret = call('GET', '/addresses/' + encodeURIComponent(held.body.address) + '/password');
    account({ address: held.body.address, password: secret.ok ? secret.body.secret : undefined });
    output.token = held.body.checkout.token;
    } else if (ACTION === 'consent') {
    const body = { marketing: MARKETING };
    if (typeof PRECHECKED !== 'undefined') body.prechecked = String(PRECHECKED) === 'true';
    if (typeof DOUBLE_OPT_IN !== 'undefined') body.double_opt_in = DOUBLE_OPT_IN;
    const recorded = call('PUT', at + 'consent', body);
    if (!recorded.ok) fail(recorded);
    } else if (ACTION === 'coupon_result') {
    const body = { result: RESULT, one_time: typeof ONE_TIME !== 'undefined' && String(ONE_TIME) === 'true' };
    if (typeof CODE !== 'undefined') body.code = CODE;
    if (typeof MESSAGE !== 'undefined') body.message = MESSAGE;
    const recorded = call('POST', at + 'coupon-result', body);
    if (!recorded.ok) fail(recorded);
    const check = recorded.body.check || { status: 'skip', detail: '' };
    output.verdict = check.status;
    if (check.status === 'fail') throw new Error('Proxy Mail QA: ' + check.detail);
    } else if (ACTION === 'return') {
    const body = { token: output.token };
    if (typeof ADD_TAGS !== 'undefined') body.add_tags = tags(ADD_TAGS);
    if (typeof REMOVE_TAGS !== 'undefined') body.remove_tags = tags(REMOVE_TAGS);
    const returned = call('POST', at + 'return', body);
    if (!returned.ok) fail(returned);
    } else {
    // Each request waits up to 10 seconds; four tries cover the email taking up to 40.
    const kind = ACTION === 'link' ? 'link' : 'code';
    const app = kind === 'link' && typeof APP_LINK !== 'undefined' && String(APP_LINK) === 'true' ? '&app=true' : '';
    const unused = typeof UNUSED !== 'undefined' && String(UNUSED) === 'true' ? '&unused=true' : '';
    const type = typeof TYPE !== 'undefined' && TYPE ? '&type=' + encodeURIComponent(TYPE) : '';
    let found = null;
    for (let i = 0; i < 4 && !found; i++) {
    // Only the last try's "nothing arrived" counts in a run's report.
    const retry = JSON.parse(JSON.stringify(headers));
    if (i < 3) retry['Proxy-Mail-QA-Run-Retry'] = 'true';
    found = call('GET', at + kind + since + '&timeout=10' + app + unused + type, null, retry);
    if (!found.ok && i < 3) found = null;
    }
    if (!found.ok) throw new Error('Proxy Mail QA: no ' + kind + ' arrived. ' + (found.body.hint || ''));
    output[kind] = found.body.value;
    }
  2. Verwende es dann in einem Ablauf: Adresse erstellen, damit registrieren und den Code eingeben.

    signup.yaml
    appId: com.example.app
    ---
    - launchApp
    - runScript:
    file: proxymail.js
    env:
    ACTION: create
    - tapOn: "Email"
    - inputText: ${output.address}
    - tapOn: "Password"
    - inputText: ${output.password}
    - tapOn: "Sign up"
    - runScript:
    file: proxymail.js
    env:
    ACTION: code
    - inputText: ${output.code}
  3. Führe sie mit deinem API-Schlüssel aus:

    Für einen Link statt eines Codes verwende ACTION: link und dann - openLink: ${output.link}, das ihn in der App öffnet. Enthält die E-Mail sowohl einen Weblink als auch einen, der die App öffnet (myapp://…), füge APP_LINK: true zu env des Skripts hinzu, um den App-Link zu bekommen.

    maestro test -e PROXYMAIL_QA_API_KEY=$PROXYMAIL_QA_API_KEY signup.yaml
- runScript:
file: proxymail.js
env:
ACTION: count
- assertTrue: ${output.count == 1}
- runScript:
file: proxymail.js
env:
ACTION: check
EXPECT_HOST: links.example.com

ACTION: check lässt den Ablauf scheitern und nennt den Grund, wenn die neueste E-Mail eine Prüfung nicht besteht: SPF, DKIM und DMARC, Links auf localhost oder einfaches http, wohin ihr Link führt (EXPECT_HOST, die Domain, die die Links deiner App nutzen), eine angegebene Gültigkeitsdauer, ihre Größe und mehr (was jede Prüfung bedeutet). ACTION: none lässt ihn scheitern, wenn innerhalb von 30 Sekunden eine E-Mail ankommt, für „Abgemeldete Nutzer bekommen nichts“; füge CATEGORY: marketing hinzu, um nur auf Marketing zu achten, damit Belege weiterhin ankommen können. Nach einem erneuten Senden überspringt UNUSED: true bei ACTION: code den bereits verwendeten Code. Nach der Registrierung für ein Angebot wartet TYPE: coupon bei ACTION: code stattdessen auf dessen Gutschein oder Aktionscode. Nach dem Einlösen im Checkout (in der Sandbox des Stores, nie mit einer echten Karte) hält ACTION: coupon_result mit RESULT: accepted (oder rejected, error), MESSAGE und ONE_TIME: true fest, was die App getan hat, setzt output.verdict und lässt den Ablauf scheitern, wenn die Prüfung coupon_result scheitert. ACTION: consent mit MARKETING: declined (und PRECHECKED, DOUBLE_OPT_IN) hält fest, wozu das Konto eingewilligt hat, damit ACTION: check sein Marketing nach den Regeln am Wohnort der Persona bewertet.

Bei ACTION: create hält ENVIRONMENT: staging fest, für welche Umgebung das Konto gedacht ist, und AGE: under18 oder COUNTRY: GB geben seiner Persona ein Geburtsdatum oder ein Land für Formulare, die danach fragen.

Mit einem gespeicherten Konto anmelden oder eines ausleihen

Abschnitt betitelt „Mit einem gespeicherten Konto anmelden oder eines ausleihen“

Für einen Anmeldeablauf setzt ACTION: signin mit TAGS: verified output.address und output.password auf die Werte eines gespeicherten Testkontos. Wenn Abläufe parallel laufen, leiht ACTION: checkout mit TAGS: verified eines aus, das kein anderer Ablauf hat, samt Passwort, und ACTION: return gibt es zurück (ADD_TAGS und REMOVE_TAGS halten fest, was der Ablauf geändert hat):

- runScript:
file: proxymail.js
env:
ACTION: checkout
TAGS: verified
# …sign in with ${output.address} and ${output.password}…
- runScript:
file: proxymail.js
env:
ACTION: return

Beide lesen gespeicherte Passwörter, also braucht der Schlüssel die Berechtigung Can read saved passwords (Project settings → API keys). Markiere Konten in der App mit ihrem Zustand, um Pools zu bilden: siehe Konto-Pools.

Mit der GitHub Action im Job übergibst du den von ihr gestarteten Lauf wie den Schlüssel und fügst TEST zu env jedes Skripts hinzu, um den Schritt zu benennen:

maestro test -e PROXYMAIL_QA_API_KEY=$PROXYMAIL_QA_API_KEY -e PROXYMAIL_QA_RUN=$PROXYMAIL_QA_RUN signup.yaml

Eine Frage zu dieser Seite?

Frag in jeder Sprache. Die Doku kann dir sofort antworten oder deine Frage an das Team von Proxy Mail QA schicken, das auf Englisch antwortet.

Doku fragen