mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-05 00:07:36 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab7e0b90d4 | ||
|
|
a3e3ae9ee1 | ||
|
|
0558b4157d | ||
|
|
ef31d6f1c9 | ||
|
|
fc73bf8334 | ||
|
|
923f9d8e87 |
@@ -81,23 +81,24 @@ tags:
|
|||||||
commands:
|
commands:
|
||||||
- tapOn: "More actions.*"
|
- tapOn: "More actions.*"
|
||||||
- tapOn: "Star|Unstar"
|
- tapOn: "Star|Unstar"
|
||||||
# A bulk action ends selection mode by itself.
|
# A bulk action ends selection mode by itself once the server has the
|
||||||
- runFlow:
|
# change, so the bar stays up for a moment: wait for it to go rather than
|
||||||
when:
|
# tap Clear selection (the tap can land after the bar is gone).
|
||||||
visible: "Clear selection"
|
- extendedWaitUntil:
|
||||||
commands:
|
notVisible: "Clear selection"
|
||||||
- tapOn: "Clear selection"
|
timeout: 15000
|
||||||
- longPressOn:
|
- longPressOn:
|
||||||
id: email-row
|
id: email-row
|
||||||
text: ".*Grüße aus München.*"
|
text: ".*Grüße aus München.*"
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: "Clear selection"
|
||||||
|
timeout: 10000
|
||||||
- runFlow:
|
- runFlow:
|
||||||
when:
|
when:
|
||||||
notVisible: "Unstar|Star"
|
notVisible: "Unstar|Star"
|
||||||
commands:
|
commands:
|
||||||
- tapOn: "More actions.*"
|
- tapOn: "More actions.*"
|
||||||
- tapOn: "Unstar|Star"
|
- tapOn: "Unstar|Star"
|
||||||
- runFlow:
|
- extendedWaitUntil:
|
||||||
when:
|
notVisible: "Clear selection"
|
||||||
visible: "Clear selection"
|
timeout: 15000
|
||||||
commands:
|
|
||||||
- tapOn: "Clear selection"
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ tags:
|
|||||||
# Each Q matches its page near the top. A PROOF the search lists under the
|
# Each Q matches its page near the top. A PROOF the search lists under the
|
||||||
# row would pass on the iPad without the page, so those differ. The Themes
|
# row would pass on the iPad without the page, so those differ. The Themes
|
||||||
# cards are one element each, named by the theme: check the page's text.
|
# cards are one element each, named by the theme: check the page's text.
|
||||||
|
# "Storage" also finds Account and Files, which puts Storage & backup third,
|
||||||
|
# under the keyboard in landscape (a tap there types on the keys): search
|
||||||
|
# "Offline", which only that page has.
|
||||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: folders, ROW: "Folders, .*", Q: "New folder", PROOF: ".*Inbox.*"}}
|
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: folders, ROW: "Folders, .*", Q: "New folder", PROOF: ".*Inbox.*"}}
|
||||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: tags, ROW: "Tags, .*", Q: "Tags", PROOF: "Email Tags"}}
|
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: tags, ROW: "Tags, .*", Q: "Tags", PROOF: "Email Tags"}}
|
||||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: downloads, ROW: "Downloads, .*", Q: "Downloads", PROOF: "Email .*eml.* filename"}}
|
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: downloads, ROW: "Downloads, .*", Q: "Downloads", PROOF: "Email .*eml.* filename"}}
|
||||||
@@ -26,7 +29,7 @@ tags:
|
|||||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: files, ROW: "Files, .*", Q: "Files", PROOF: "Default View"}}
|
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: files, ROW: "Files, .*", Q: "Files", PROOF: "Default View"}}
|
||||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: sidebar-apps, ROW: "Sidebar Apps, .*", Q: "Sidebar", PROOF: "Keep Apps Loaded"}}
|
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: sidebar-apps, ROW: "Sidebar Apps, .*", Q: "Sidebar", PROOF: "Keep Apps Loaded"}}
|
||||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: themes, ROW: "Themes, .*", Q: "Themes", PROOF: "Pick a colou?r theme.*"}}
|
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: themes, ROW: "Themes, .*", Q: "Themes", PROOF: "Pick a colou?r theme.*"}}
|
||||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: storage, ROW: "Storage & backup, .*", Q: "Storage", PROOF: "Offline mail.*"}}
|
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: storage, ROW: "Storage & backup, .*", Q: "Offline", PROOF: "Offline mail.*"}}
|
||||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: about, ROW: "About, .*", Q: "About", PROOF: "This device"}}
|
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: about, ROW: "About, .*", Q: "About", PROOF: "This device"}}
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: tab-mail
|
id: tab-mail
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ appId: org.bulwarkmail.mobile
|
|||||||
- inputText: ${Q}
|
- inputText: ${Q}
|
||||||
- waitForAnimationToEnd:
|
- waitForAnimationToEnd:
|
||||||
timeout: 3000
|
timeout: 3000
|
||||||
# Now and then the simulator drops or garbles a typed character
|
# Now and then the simulator drops or garbles a typed character:
|
||||||
# (seen: "Storagew,"): type it again, a few times if need be.
|
# type it again, a few times if need be.
|
||||||
- repeat:
|
- repeat:
|
||||||
while:
|
while:
|
||||||
notVisible: ${ROW}
|
notVisible: ${ROW}
|
||||||
|
|||||||
+6
-7
@@ -57,13 +57,12 @@ module.exports = {
|
|||||||
supportsTablet: true,
|
supportsTablet: true,
|
||||||
bundleIdentifier: 'org.bulwarkmail.mobile',
|
bundleIdentifier: 'org.bulwarkmail.mobile',
|
||||||
buildNumber: IOS_BUILD_NUMBER,
|
buildNumber: IOS_BUILD_NUMBER,
|
||||||
// No usesNonExemptEncryption here. PGP and S/MIME (#58) are non-exempt
|
config: {
|
||||||
// encryption, but only standard algorithms from open-source libraries
|
// PGP and S/MIME (#58) add end-to-end encryption beyond HTTPS/TLS,
|
||||||
// and the app is not on the French store, so App Store Connect keeps no
|
// so the app declares non-exempt encryption; App Store Connect asks
|
||||||
// declaration and has no ITSEncryptionExportComplianceCode to give. An
|
// for the export compliance documentation on upload.
|
||||||
// Info.plist saying YES without that code fails upload validation
|
usesNonExemptEncryption: true,
|
||||||
// (90592), so the key stays out and scripts/testflight-public.mjs
|
},
|
||||||
// answers the question for each build through the API.
|
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
adaptiveIcon: {
|
adaptiveIcon: {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bulwark-mobile",
|
"name": "bulwark-mobile",
|
||||||
"version": "0.1.89",
|
"version": "0.1.87",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bulwark-mobile",
|
"name": "bulwark-mobile",
|
||||||
"version": "0.1.89",
|
"version": "0.1.87",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bulwark-mobile",
|
"name": "bulwark-mobile",
|
||||||
"version": "0.1.89",
|
"version": "0.1.87",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -43,14 +43,16 @@ function findHierarchy(obj, seen = new Set()) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of walkFiles(root).filter((f) => /commands-.*\.json$/.test(f))) {
|
// Maestro 1 writes commands-<flow>.json, Maestro 2 <flow>/commands.json.
|
||||||
|
for (const file of walkFiles(root).filter((f) => /(^|[\\/])commands(-[^\\/]*)?\.json$/.test(f))) {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
out.push(`== ${path.basename(file)}`);
|
const base = path.basename(file);
|
||||||
|
out.push(`== ${base === 'commands.json' ? `${path.basename(path.dirname(file))} / ${base}` : base}`);
|
||||||
for (const entry of Array.isArray(data) ? data : []) {
|
for (const entry of Array.isArray(data) ? data : []) {
|
||||||
const command = entry.command || {};
|
const command = entry.command || {};
|
||||||
const type = Object.keys(command).find((k) => command[k]) || '?';
|
const type = Object.keys(command).find((k) => command[k]) || '?';
|
||||||
|
|||||||
+31
-11
@@ -8,8 +8,8 @@
|
|||||||
# $OUT/screenshots, $OUT/run.mp4, $OUT/app.log, $OUT/steps.txt.
|
# $OUT/screenshots, $OUT/run.mp4, $OUT/app.log, $OUT/steps.txt.
|
||||||
#
|
#
|
||||||
# Each flow runs in its own Maestro process with a time cap: when the XCTest
|
# Each flow runs in its own Maestro process with a time cap: when the XCTest
|
||||||
# driver dies (it does, now and then), only that flow fails and the next one
|
# driver dies (it does, now and then), only that flow is affected: it runs
|
||||||
# starts a fresh driver.
|
# once more with a fresh driver, and the next flow starts a fresh one too.
|
||||||
#
|
#
|
||||||
# Secrets: Maestro's debug output holds every typed text, passwords included,
|
# Secrets: Maestro's debug output holds every typed text, passwords included,
|
||||||
# and artifacts of this public repo are public. Only the warm-up (which types
|
# and artifacts of this public repo are public. Only the warm-up (which types
|
||||||
@@ -75,23 +75,43 @@ if [ $STATUS -eq 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
FAILED=()
|
FAILED=()
|
||||||
|
RETRIED=()
|
||||||
for f in "${FLOWS[@]}"; do
|
for f in "${FLOWS[@]}"; do
|
||||||
name=$(basename "$f" .yaml)
|
name=$(basename "$f" .yaml)
|
||||||
started=$(date +%s)
|
# When the XCTest driver dies during a flow ("Device became unreachable"),
|
||||||
echo "::group::$name"
|
# the flow gets one more try in a fresh Maestro process, which starts a
|
||||||
(cd "$OUT/screenshots" && capped maestro --device "$UDID" test \
|
# new driver. Any other failure (an assertion, a missing element, the
|
||||||
--format junit --output "$OUT/junit/$name.xml" \
|
# time cap) stays a failure. The driver's death shows in Maestro's log in
|
||||||
--debug-output "$DEBUG_DIR/$name" \
|
# the flow's debug output (never uploaded; grep -q prints nothing). Piping
|
||||||
"${ARGS[@]}" "$f")
|
# Maestro's console output instead loses the failure reasons it prints.
|
||||||
rc=$?
|
for attempt in 1 2; do
|
||||||
echo "::endgroup::"
|
suffix=""
|
||||||
echo "$name: exit $rc after $(( $(date +%s) - started )) s"
|
[ $attempt -gt 1 ] && suffix="-retry"
|
||||||
|
started=$(date +%s)
|
||||||
|
echo "::group::$name$suffix"
|
||||||
|
(cd "$OUT/screenshots" && capped maestro --device "$UDID" test \
|
||||||
|
--format junit --output "$OUT/junit/$name.xml" \
|
||||||
|
--debug-output "$DEBUG_DIR/$name$suffix" \
|
||||||
|
"${ARGS[@]}" "$f")
|
||||||
|
rc=$?
|
||||||
|
echo "::endgroup::"
|
||||||
|
echo "$name$suffix: exit $rc after $(( $(date +%s) - started )) s"
|
||||||
|
[ $rc -eq 0 ] && break
|
||||||
|
[ $attempt -eq 2 ] && break
|
||||||
|
grep -rqE 'DeviceUnreachableException|Device became unreachable|Transport unreachable' \
|
||||||
|
"$DEBUG_DIR/$name$suffix" 2>/dev/null || break
|
||||||
|
echo "::warning::$name: the Maestro driver became unreachable, running the flow once more"
|
||||||
|
RETRIED+=("$name")
|
||||||
|
done
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
FAILED+=("$name")
|
FAILED+=("$name")
|
||||||
# What the screen showed when it failed.
|
# What the screen showed when it failed.
|
||||||
xcrun simctl io "$UDID" screenshot "$OUT/screenshots/FAILED-$name.png" >/dev/null 2>&1 || true
|
xcrun simctl io "$UDID" screenshot "$OUT/screenshots/FAILED-$name.png" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
if [ ${#RETRIED[@]} -gt 0 ]; then
|
||||||
|
echo "::warning::Flows run again after the driver died: ${RETRIED[*]}"
|
||||||
|
fi
|
||||||
if [ ${#FAILED[@]} -gt 0 ]; then
|
if [ ${#FAILED[@]} -gt 0 ]; then
|
||||||
echo "::error::Failed flows: ${FAILED[*]}"
|
echo "::error::Failed flows: ${FAILED[*]}"
|
||||||
STATUS=1
|
STATUS=1
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ if (!group) {
|
|||||||
// ── Wait for the build ─────────────────────────────────────────
|
// ── Wait for the build ─────────────────────────────────────────
|
||||||
const query =
|
const query =
|
||||||
`/v1/builds?filter[app]=${app.id}&filter[version]=${encodeURIComponent(buildNumber)}` +
|
`/v1/builds?filter[app]=${app.id}&filter[version]=${encodeURIComponent(buildNumber)}` +
|
||||||
`&filter[preReleaseVersion.version]=${encodeURIComponent(version)}&fields[builds]=version,processingState,usesNonExemptEncryption`;
|
`&filter[preReleaseVersion.version]=${encodeURIComponent(version)}&fields[builds]=version,processingState`;
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
let build;
|
let build;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -121,18 +121,6 @@ for (;;) {
|
|||||||
}
|
}
|
||||||
console.log(`Build ${version} (${buildNumber}) is processed.`);
|
console.log(`Build ${version} (${buildNumber}) is processed.`);
|
||||||
|
|
||||||
// ── Export compliance ──────────────────────────────────────────
|
|
||||||
// The app uses non-exempt encryption (PGP and S/MIME, #58) with standard
|
|
||||||
// algorithms only and is not on the French store: App Store Connect needs no
|
|
||||||
// documentation, so Info.plist has no key and the answer is given here.
|
|
||||||
if (build.attributes.usesNonExemptEncryption == null) {
|
|
||||||
const answer = await api('PATCH', `/v1/builds/${build.id}`, {
|
|
||||||
data: { type: 'builds', id: build.id, attributes: { usesNonExemptEncryption: true } },
|
|
||||||
});
|
|
||||||
if (!answer.ok) fail('Answering export compliance', answer);
|
|
||||||
console.log('Export compliance answered: standard encryption, no documentation needed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Add it to the group ────────────────────────────────────────
|
// ── Add it to the group ────────────────────────────────────────
|
||||||
const add = await api('POST', `/v1/betaGroups/${group.id}/relationships/builds`, {
|
const add = await api('POST', `/v1/betaGroups/${group.id}/relationships/builds`, {
|
||||||
data: [{ type: 'builds', id: build.id }],
|
data: [{ type: 'builds', id: build.id }],
|
||||||
@@ -162,7 +150,7 @@ const detail = await api('GET', `/v1/builds/${build.id}/buildBetaDetail`);
|
|||||||
if (!detail.ok) fail('Reading the build state', detail);
|
if (!detail.ok) fail('Reading the build state', detail);
|
||||||
const external = detail.json.data.attributes.externalBuildState;
|
const external = detail.json.data.attributes.externalBuildState;
|
||||||
if (external === 'MISSING_EXPORT_COMPLIANCE') {
|
if (external === 'MISSING_EXPORT_COMPLIANCE') {
|
||||||
console.error('The build still has no export compliance answer; answer it in App Store Connect > TestFlight.');
|
console.error('The build has no export compliance answer; set usesNonExemptEncryption in app.config.js.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (external === 'READY_FOR_BETA_SUBMISSION') {
|
if (external === 'READY_FOR_BETA_SUBMISSION') {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ vi.mock('../booking-calendar-actions', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import type { Email } from '../../../../api/types';
|
import type { Email } from '../../../../api/types';
|
||||||
import { usePolicyStore } from '../../../../stores/policy-store';
|
|
||||||
import { useSettingsStore } from '../../../../stores/settings-store';
|
import { useSettingsStore } from '../../../../stores/settings-store';
|
||||||
import { StructuredMailBanner } from '../StructuredMailBanner';
|
import { StructuredMailBanner } from '../StructuredMailBanner';
|
||||||
import { byTestId, render, textOf } from './render-native';
|
import { byTestId, render, textOf } from './render-native';
|
||||||
@@ -38,11 +37,7 @@ function mail(ar: string | null, subject = 'Your order'): Email {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialSettings = useSettingsStore.getState();
|
const initialSettings = useSettingsStore.getState();
|
||||||
const initialPolicy = usePolicyStore.getState();
|
afterEach(() => useSettingsStore.setState(initialSettings, true));
|
||||||
afterEach(() => {
|
|
||||||
useSettingsStore.setState(initialSettings, true);
|
|
||||||
usePolicyStore.setState(initialPolicy, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('StructuredMailBanner', () => {
|
describe('StructuredMailBanner', () => {
|
||||||
it('shows the order card of a verified mail', () => {
|
it('shows the order card of a verified mail', () => {
|
||||||
@@ -72,8 +67,6 @@ describe('StructuredMailBanner', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads product images only when remote content would load', () => {
|
it('loads product images only when remote content would load', () => {
|
||||||
// The webmail's config is known and allows third-party traffic.
|
|
||||||
usePolicyStore.setState({ configPending: false, links: {} });
|
|
||||||
useSettingsStore.setState({ externalContentPolicy: 'block', trustedSenders: [] });
|
useSettingsStore.setState({ externalContentPolicy: 'block', trustedSenders: [] });
|
||||||
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(0);
|
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(0);
|
||||||
useSettingsStore.setState({ trustedSenders: ['orders@shop.example'] });
|
useSettingsStore.setState({ trustedSenders: ['orders@shop.example'] });
|
||||||
@@ -81,12 +74,4 @@ describe('StructuredMailBanner', () => {
|
|||||||
useSettingsStore.setState({ externalContentPolicy: 'allow', trustedSenders: [] });
|
useSettingsStore.setState({ externalContentPolicy: 'allow', trustedSenders: [] });
|
||||||
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(1);
|
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads no product images while the webmail config is unknown or forbids third-party traffic', () => {
|
|
||||||
useSettingsStore.setState({ externalContentPolicy: 'allow', trustedSenders: [] });
|
|
||||||
usePolicyStore.setState({ configPending: true, links: {} });
|
|
||||||
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(0);
|
|
||||||
usePolicyStore.setState({ configPending: false, links: { noThirdPartyTraffic: true } });
|
|
||||||
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,22 +2,13 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
// PGP and S/MIME (#58) are non-exempt encryption with standard algorithms, and
|
// With PGP and S/MIME in the app (#58) the encryption is no longer only
|
||||||
// the app is not on the French store, so App Store Connect has no declaration
|
// HTTPS/TLS, so the App Store export compliance answer must say so.
|
||||||
// and no compliance code. Info.plist must not claim YES (upload validation
|
|
||||||
// rejects it without the code, 90592) nor NO (that would be untrue); the
|
|
||||||
// TestFlight script answers per build instead.
|
|
||||||
|
|
||||||
const root = path.resolve(__dirname, '../../..');
|
|
||||||
|
|
||||||
describe('export compliance', () => {
|
describe('export compliance', () => {
|
||||||
it('leaves the encryption key out of the iOS config', () => {
|
it('declares non-exempt encryption for iOS', () => {
|
||||||
const src = fs.readFileSync(path.join(root, 'app.config.js'), 'utf8');
|
const src = fs.readFileSync(path.resolve(__dirname, '../../../app.config.js'), 'utf8');
|
||||||
expect(src).not.toMatch(/usesNonExemptEncryption\s*:/);
|
expect(src).toMatch(/usesNonExemptEncryption: true/);
|
||||||
});
|
expect(src).not.toMatch(/usesNonExemptEncryption: false/);
|
||||||
|
|
||||||
it('answers non-exempt encryption for each TestFlight build', () => {
|
|
||||||
const src = fs.readFileSync(path.join(root, 'scripts/testflight-public.mjs'), 'utf8');
|
|
||||||
expect(src).toMatch(/attributes: \{ usesNonExemptEncryption: true \}/);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user