chore(networkConfig): Fix android crash on empty subdomains config (#46)

## Background
The app crashes when `detox` plugin is configured with an empty subdomain in cases where it's desired to allow all traffic.

## Cause
```
      [
        "@config-plugins/detox",
        {
          "subdomains": []
        }
      ]
```
The configuration above generates an invalid xml resulting in a  `Caused by: [android.security.net](http://android.security.net/).config.XmlConfigSource$ParserException: No domain elements in domain-config at: Binary XML file line #3` as it generates

```
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    
        <domain-config cleartextTrafficPermitted="true">
           
        </domain-config>
        
</network-security-config>
```


## Fix
This PR inserts a `base-config` instead of a `domain-config` when subdomains is "*" to allow all traffic.
```
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

```

It also omits making the network security config XML file when subdomains is an empty array.
This commit is contained in:
Gozie Ochu authored and GitHub committed 2022-03-02 20:14:19 -08:00
1 parent ec2bbedf8b
commit 88e0cd0ccf
8 files changed
+173 -29

No files matched your search

+23
View File
@@ -53,6 +53,29 @@ Detox is an end-to-end (e2e) testing library for iOS and Android. You can use it
- Generate the native code `expo prebuild`
- Run `yarn detox init -r jest`
## API
The plugin provides props for extra customization. Every time you change the props or plugins, you'll need to rebuild (and `prebuild`) the native app. If no extra properties are added, defaults will be used.
- `skipProguard` (_boolean_): Disable adding proguard minification to the `app/build.gradle`. Defaults to `false`.
- `subdomains` (_string[] | '*'_): Hostnames to add to the network security config. Pass `'*'` to allow all domains. Defaults to `['10.0.2.2', 'localhost']`.
`app.config.js`
```ts
export default {
plugins: [
[
"@config-plugins/detox",
{
skipProguard: false,
subdomains: ["10.0.2.2", "localhost"],
},
],
],
};
```
## FAQ
If the following commands fail, you can get better debug info by running a subset command:
+3 -1
View File
@@ -1,4 +1,5 @@
import { ConfigPlugin } from "@expo/config-plugins";
import { SubdomainsType } from "./withNetworkSecurityConfig";
declare const _default: ConfigPlugin<void | {
/**
* Disable adding proguard minification to the `app/build.gradle`.
@@ -9,9 +10,10 @@ declare const _default: ConfigPlugin<void | {
/**
* Subdomains to add to the network security config.
* Pass `['10.0.3.2', 'localhost']` to use Genymotion emulators instead of Google emulators.
* Pass `*` to allow all domains.
*
* @default ['10.0.2.2', 'localhost'] // (Google emulators)
*/
subdomains?: string[] | undefined;
subdomains?: SubdomainsType | undefined;
}>;
export default _default;
+3 -2
View File
@@ -1,8 +1,9 @@
import { ConfigPlugin } from "@expo/config-plugins";
export declare function getTemplateFile(subdomains: string[]): string;
export declare type SubdomainsType = string[] | "*";
export declare function getTemplateFile(subdomains: SubdomainsType): string;
/**
* [Step 6](https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md#6-enable-clear-text-unencrypted-traffic-for-detox). Link the `network_security_config.xml` file to the `AndroidManifest.xml`.
*/
export declare const withNetworkSecurityConfigManifest: ConfigPlugin<{
subdomains: string[];
subdomains: SubdomainsType;
} | void>;
+21 -6
View File
@@ -27,16 +27,27 @@ const config_plugins_1 = require("@expo/config-plugins");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const assert_1 = __importDefault(require("assert"));
function getTemplateFile(subdomains) {
return `<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
function getTemplateConfigContent(subdomains) {
if (subdomains === "*") {
// allow all domains
return '<base-config cleartextTrafficPermitted="true" />';
}
return `
<domain-config cleartextTrafficPermitted="true">
${subdomains
${subdomains
.map((subdomain) => `<domain includeSubdomains="true">${subdomain}</domain>`)
.join("")}
</domain-config>
</network-security-config>
`;
`;
}
function getTemplateFile(subdomains) {
const content = getTemplateConfigContent(subdomains);
return `
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
${content}
</network-security-config>
`;
}
exports.getTemplateFile = getTemplateFile;
/**
@@ -65,6 +76,10 @@ const withNetworkSecurityConfigManifest = (config, props) => {
// https://developer.android.com/training/articles/security-config
props = { subdomains: ["10.0.2.2", "localhost"] };
}
if (typeof props.subdomains === "object" && !props.subdomains.length) {
// if subdomains is an empty array, skip network config mod
return config;
}
config = withNetworkSecurityConfigFile(config, props);
return config_plugins_1.withAndroidManifest(config, (config) => {
const application = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getTemplateFile returns domain-config when subdomains is an array 1`] = `
"
<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
<network-security-config>
<domain-config cleartextTrafficPermitted=\\"true\\">
<domain includeSubdomains=\\"true\\">localhost</domain>
</domain-config>
</network-security-config>
"
`;
exports[`getTemplateFile returns only base-config when subdomains is '*' 1`] = `
"
<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
<network-security-config>
<base-config cleartextTrafficPermitted=\\"true\\" />
</network-security-config>
"
`;
exports[`withNetworkSecurityConfigManifest doesn't modify config if subdomains is an empty array 1`] = `
Object {
"name": "foo",
"slug": "bar",
}
`;
exports[`withNetworkSecurityConfigManifest modifies config if subdomains is not empty 1`] = `
Object {
"mods": Object {
"android": Object {
"dangerous": [Function],
"manifest": [Function],
},
},
"name": "foo",
"slug": "bar",
}
`;
@@ -0,0 +1,38 @@
import {
getTemplateFile,
withNetworkSecurityConfigManifest,
} from "../withNetworkSecurityConfig";
describe(getTemplateFile, () => {
it(`returns only base-config when subdomains is '*'`, () => {
const content = getTemplateFile("*");
expect(content).toMatchSnapshot();
expect(content).toMatch(/base\-config/);
expect(content).not.toMatch(/domain\-config/);
});
it(`returns domain-config when subdomains is an array`, () => {
const content = getTemplateFile(["localhost"]);
expect(content).toMatchSnapshot();
expect(content).toMatch(/domain\-config/);
expect(content).toMatch(/localhost/);
});
});
const exp = { name: "foo", slug: "bar" };
describe(withNetworkSecurityConfigManifest, () => {
it(`doesn't modify config if subdomains is an empty array`, () => {
const applied = withNetworkSecurityConfigManifest(exp, {
subdomains: [],
});
expect(applied).toMatchSnapshot();
expect(applied).toStrictEqual(exp);
});
it(`modifies config if subdomains is not empty`, () => {
const applied = withNetworkSecurityConfigManifest(exp, {
subdomains: ["10.0.2.2"],
});
expect(applied).toMatchSnapshot();
expect(applied).toHaveProperty("mods.android");
});
});
+3 -2
View File
@@ -8,7 +8,7 @@ import withDetoxProjectGradle from "./withDetoxProjectGradle";
import withDetoxTestAppGradle from "./withDetoxTestAppGradle";
import { withDetoxTestClass } from "./withDetoxTestClass";
import withKotlinGradle from "./withKotlinGradle";
import { withNetworkSecurityConfigManifest } from "./withNetworkSecurityConfig";
import { withNetworkSecurityConfigManifest, SubdomainsType } from "./withNetworkSecurityConfig";
import withProguardGradle from "./withProguardGradle";
const withDetox: ConfigPlugin<{
@@ -21,10 +21,11 @@ const withDetox: ConfigPlugin<{
/**
* Subdomains to add to the network security config.
* Pass `['10.0.3.2', 'localhost']` to use Genymotion emulators instead of Google emulators.
* Pass `*` to allow all domains.
*
* @default ['10.0.2.2', 'localhost'] // (Google emulators)
*/
subdomains?: string[];
subdomains?: SubdomainsType;
} | void> = (config, { skipProguard, subdomains } = {}) => {
return withPlugins(
config,
+39 -18
View File
@@ -8,28 +8,41 @@ import * as path from "path";
import * as fs from "fs";
import assert from "assert";
export function getTemplateFile(subdomains: string[]): string {
return `<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
export type SubdomainsType = string[] | "*";
function getTemplateConfigContent(subdomains: SubdomainsType) {
if (subdomains === "*") {
// allow all domains
return '<base-config cleartextTrafficPermitted="true" />';
}
return `
<domain-config cleartextTrafficPermitted="true">
${subdomains
.map(
(subdomain) =>
`<domain includeSubdomains="true">${subdomain}</domain>`
)
.join("")}
${subdomains
.map(
(subdomain) =>
`<domain includeSubdomains="true">${subdomain}</domain>`
)
.join("")}
</domain-config>
</network-security-config>
`;
`;
}
export function getTemplateFile(subdomains: SubdomainsType): string {
const content = getTemplateConfigContent(subdomains);
return `
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
${content}
</network-security-config>
`;
}
/**
* Create `network_security_config.xml` resource file.
*/
const withNetworkSecurityConfigFile: ConfigPlugin<{ subdomains: string[] }> = (
config,
{ subdomains }
) => {
const withNetworkSecurityConfigFile: ConfigPlugin<{
subdomains: SubdomainsType;
}> = (config, { subdomains }) => {
return withDangerousMod(config, [
"android",
async (config) => {
@@ -53,14 +66,22 @@ const withNetworkSecurityConfigFile: ConfigPlugin<{ subdomains: string[] }> = (
/**
* [Step 6](https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md#6-enable-clear-text-unencrypted-traffic-for-detox). Link the `network_security_config.xml` file to the `AndroidManifest.xml`.
*/
export const withNetworkSecurityConfigManifest: ConfigPlugin<{
subdomains: string[];
} | void> = (config, props) => {
export const withNetworkSecurityConfigManifest: ConfigPlugin<
{
subdomains: SubdomainsType;
} | void
> = (config, props) => {
if (!props || !props.subdomains) {
// (*) 10.0.2.2 for Google emulators, 10.0.3.2 for Genymotion emulators.
// https://developer.android.com/training/articles/security-config
props = { subdomains: ["10.0.2.2", "localhost"] };
}
if (typeof props.subdomains === "object" && !props.subdomains.length) {
// if subdomains is an empty array, skip network config mod
return config;
}
config = withNetworkSecurityConfigFile(config, props);
return withAndroidManifest(config, (config) => {
const application = AndroidConfig.Manifest.getMainApplicationOrThrow(