Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions packages/build-tools/src/utils/__tests__/appConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { bunyan } from '@expo/logger';
import spawnAsync from '@expo/turtle-spawn';

import { Datadog } from '../../datadog';
import { readAppConfig } from '../appConfig';

jest.mock('@expo/env', () => ({
Expand All @@ -13,6 +15,13 @@ jest.mock('@expo/config', () => ({
}));

jest.mock('../expoCli');
jest.mock('@expo/turtle-spawn');

jest.mock('../../datadog', () => ({
Datadog: {
log: jest.fn(),
},
}));

const { expoCommandAsync } = jest.requireMock('../expoCli') as {
expoCommandAsync: jest.Mock;
Expand All @@ -26,6 +35,9 @@ const { load: loadEnv } = jest.requireMock('@expo/env') as {
load: jest.Mock;
};

const datadogLogMock = jest.mocked(Datadog.log);
const spawnAsyncMock = jest.mocked(spawnAsync);

const logger = { warn: jest.fn(), info: jest.fn(), error: jest.fn() } as unknown as bunyan;

const baseParams = {
Expand All @@ -34,7 +46,29 @@ const baseParams = {
logger,
};

function getCloudEnv(buildId: string) {
return {
NODE_ENV: 'development',
EAS_BUILD_RUNNER: 'eas-build',
EAS_BUILD_ID: buildId,
};
}

function getEnvWorkerResult(env: Record<string, string> = {}) {
return {
stdout: JSON.stringify(env),
} as never;
}

describe(readAppConfig, () => {
beforeEach(() => {
jest.resetAllMocks();
loadEnv.mockReturnValue({ FROM_DOTENV: 'true' });
getConfig.mockReturnValue({
exp: { name: 'fallback-app', slug: 'fallback-app' },
});
});

it('returns config from expo CLI when it succeeds', async () => {
const config = { exp: { name: 'test-app', slug: 'test-app' } };
expoCommandAsync.mockResolvedValue({ stdout: JSON.stringify(config) });
Expand Down Expand Up @@ -105,4 +139,193 @@ describe(readAppConfig, () => {

expect(loadEnv).not.toHaveBeenCalled();
});

it("doesn't compare app config for local builds", async () => {
const config = { exp: { name: 'test-app', slug: 'test-app' } };
expoCommandAsync.mockResolvedValue({ stdout: JSON.stringify(config) });

await readAppConfig({
...baseParams,
env: {
NODE_ENV: 'development',
EAS_BUILD_RUNNER: 'local-build-plugin',
EAS_BUILD_ID: 'local-build',
},
});

expect(expoCommandAsync).toHaveBeenCalledTimes(1);
expect(spawnAsyncMock).not.toHaveBeenCalled();
expect(datadogLogMock).not.toHaveBeenCalled();
});

it('logs a match when app config is the same in production mode', async () => {
const config = { exp: { name: 'test-app', slug: 'test-app' } };
const env = getCloudEnv('matching-build');
expoCommandAsync.mockResolvedValue({ stdout: JSON.stringify(config) });

const result = await readAppConfig({ ...baseParams, env });

expect(result).toEqual(config);
expect(expoCommandAsync).toHaveBeenNthCalledWith(
2,
'/project',
['config', '--json', '--full', '--type', 'public'],
{
env: {
NODE_ENV: 'production',
EAS_BUILD_RUNNER: 'eas-build',
EAS_BUILD_ID: 'matching-build',
__EXPO_CONFIG_MODE: 'production',
},
}
);
expect(datadogLogMock).toHaveBeenCalledWith('App config production mode comparison match', {
event: 'app_config_production_mode_comparison',
status: 'match',
current_source: 'expo-cli',
production_source: 'expo-cli',
});
});

it('keeps the current app config and logs a production mode mismatch', async () => {
const currentConfig = { exp: { name: 'current-app', slug: 'test-app' } };
const productionConfig = { exp: { name: 'production-app', slug: 'test-app' } };
const env = {
...getCloudEnv('mismatching-build'),
FROM_BUILD: 'true',
};
loadEnv.mockReturnValue({ FROM_CURRENT_DOTENV: 'true' });
spawnAsyncMock.mockResolvedValue(
getEnvWorkerResult({
FROM_BUILD: 'from-dotenv',
FROM_PRODUCTION_DOTENV: 'true',
})
);
expoCommandAsync
.mockResolvedValueOnce({ stdout: JSON.stringify(currentConfig) })
.mockResolvedValueOnce({ stdout: JSON.stringify(productionConfig) });

const result = await readAppConfig({
...baseParams,
env,
sdkVersion: '49.0.0',
});

expect(result).toEqual(currentConfig);
expect(env).toEqual({
NODE_ENV: 'development',
EAS_BUILD_RUNNER: 'eas-build',
EAS_BUILD_ID: 'mismatching-build',
FROM_BUILD: 'true',
});
expect(spawnAsyncMock).toHaveBeenCalledWith(
process.execPath,
[expect.stringMatching(/appConfigEnvWorker\.js$/), '/project'],
{
cwd: '/project',
env: {
NODE_ENV: 'production',
EAS_BUILD_RUNNER: 'eas-build',
EAS_BUILD_ID: 'mismatching-build',
FROM_BUILD: 'true',
__EXPO_CONFIG_MODE: 'production',
},
stdio: 'pipe',
}
);
expect(expoCommandAsync).toHaveBeenNthCalledWith(
2,
'/project',
['config', '--json', '--full', '--type', 'public'],
{
env: {
NODE_ENV: 'production',
EAS_BUILD_RUNNER: 'eas-build',
EAS_BUILD_ID: 'mismatching-build',
FROM_BUILD: 'true',
FROM_PRODUCTION_DOTENV: 'true',
__EXPO_CONFIG_MODE: 'production',
},
}
);
expect(datadogLogMock).toHaveBeenCalledWith('App config production mode comparison mismatch', {
event: 'app_config_production_mode_comparison',
status: 'mismatch',
current_source: 'expo-cli',
production_source: 'expo-cli',
});
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('current-app');
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('production-app');
});

it("doesn't compare app config when the current read uses bundled @expo/config", async () => {
const config = { exp: { name: 'test-app', slug: 'test-app' } };
expoCommandAsync.mockRejectedValue(new Error('expo not found'));
getConfig.mockReturnValue(config);

const result = await readAppConfig({
...baseParams,
env: getCloudEnv('bundled-config-build'),
});

expect(result).toEqual(config);
expect(spawnAsyncMock).not.toHaveBeenCalled();
expect(datadogLogMock).toHaveBeenCalledWith('App config production mode comparison error', {
event: 'app_config_production_mode_comparison',
status: 'error',
current_source: 'bundled-config',
reason: 'current_read_used_fallback',
});
});

it('keeps the current app config when the production read fails', async () => {
const currentConfig = { exp: { name: 'current-app', slug: 'test-app' } };
expoCommandAsync
.mockResolvedValueOnce({ stdout: JSON.stringify(currentConfig) })
.mockRejectedValueOnce(new Error('production config failed'));

const result = await readAppConfig({
...baseParams,
env: getCloudEnv('failed-production-build'),
});

expect(result).toEqual(currentConfig);
expect(logger.warn).not.toHaveBeenCalled();
expect(datadogLogMock).toHaveBeenCalledWith('App config production mode comparison error', {
event: 'app_config_production_mode_comparison',
status: 'error',
current_source: 'expo-cli',
reason: 'production_read_failed',
});
});

it("doesn't compare app config more than once for the same build", async () => {
const config = { exp: { name: 'test-app', slug: 'test-app' } };
const params = {
...baseParams,
env: getCloudEnv('repeated-build'),
};
expoCommandAsync.mockResolvedValue({ stdout: JSON.stringify(config) });

await readAppConfig(params);
await readAppConfig(params);

expect(expoCommandAsync).toHaveBeenCalledTimes(3);
expect(datadogLogMock).toHaveBeenCalledTimes(1);
});

it("doesn't fail the build when Datadog logging fails", async () => {
const config = { exp: { name: 'test-app', slug: 'test-app' } };
expoCommandAsync.mockResolvedValue({ stdout: JSON.stringify(config) });
datadogLogMock.mockImplementation(() => {
throw new Error('Datadog failed');
});

await expect(
readAppConfig({
...baseParams,
env: getCloudEnv('datadog-failure-build'),
})
).resolves.toEqual(config);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { get } from '@expo/env';

import { runAppConfigEnvWorker } from '../appConfigEnvWorker';

jest.mock('@expo/env', () => ({
get: jest.fn(),
}));

const getEnvMock = jest.mocked(get);

describe(runAppConfigEnvWorker, () => {
it('writes the production dotenv vars as JSON', () => {
const stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
getEnvMock.mockReturnValue({
env: { FROM_PRODUCTION_DOTENV: 'true' },
files: ['/project/.env.production'],
});

try {
runAppConfigEnvWorker('/project');

expect(getEnvMock).toHaveBeenCalledWith('/project', {
force: true,
silent: true,
});
expect(stdoutWriteSpy).toHaveBeenCalledWith(
JSON.stringify({ FROM_PRODUCTION_DOTENV: 'true' })
);
} finally {
stdoutWriteSpy.mockRestore();
}
});
});
Loading
Loading