apify-test-tools 0.9.1-beta.3 → 0.9.1-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +3 -0
- package/.vscode/settings.json +2 -1
- package/CHANGELOG.md +4 -0
- package/bin/main.ts +2 -1
- package/bin/types.ts +0 -11
- package/bin/utils/config/load-config.ts +251 -0
- package/bin/utils/json-file.ts +55 -0
- package/bin/utils.ts +0 -138
- package/dist/bin/main.d.ts.map +1 -1
- package/dist/bin/main.js +2 -1
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/types.d.ts +0 -9
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.d.ts +44 -0
- package/dist/bin/utils/config/load-config.d.ts.map +1 -0
- package/dist/bin/utils/config/load-config.js +177 -0
- package/dist/bin/utils/config/load-config.js.map +1 -0
- package/dist/bin/utils/json-file.d.ts +20 -0
- package/dist/bin/utils/json-file.d.ts.map +1 -0
- package/dist/bin/utils/json-file.js +42 -0
- package/dist/bin/utils/json-file.js.map +1 -0
- package/dist/bin/utils.d.ts +0 -6
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +0 -98
- package/dist/bin/utils.js.map +1 -1
- package/dist/lib/consts.d.ts +1 -0
- package/dist/lib/consts.d.ts.map +1 -1
- package/dist/lib/consts.js +3 -0
- package/dist/lib/consts.js.map +1 -1
- package/dist/lib/lib.d.ts +4 -0
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +12 -16
- package/dist/lib/lib.js.map +1 -1
- package/dist/lib/types.d.ts +11 -3
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/test/unit/bin/utils/actor-config.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/actor-config.test.d.ts.map +1 -0
- package/dist/test/unit/bin/{utils.test.js → utils/actor-config.test.js} +126 -51
- package/dist/test/unit/bin/utils/actor-config.test.js.map +1 -0
- package/dist/test/unit/bin/utils/files.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/files.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/files.test.js +117 -0
- package/dist/test/unit/bin/utils/files.test.js.map +1 -0
- package/dist/test/unit/lib.test.d.ts +2 -0
- package/dist/test/unit/lib.test.d.ts.map +1 -0
- package/dist/test/unit/lib.test.js +37 -0
- package/dist/test/unit/lib.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/consts.ts +4 -0
- package/lib/lib.ts +20 -18
- package/lib/types.ts +11 -3
- package/package.json +3 -2
- package/test/fixtures/bin/utils/files/array.json +1 -0
- package/test/fixtures/bin/utils/files/comments.jsonc +4 -0
- package/test/fixtures/bin/utils/files/directory.json/.gitkeep +0 -0
- package/test/fixtures/bin/utils/files/empty-object.json +1 -0
- package/test/fixtures/bin/utils/files/empty.json +0 -0
- package/test/fixtures/bin/utils/files/malformed.json +1 -0
- package/test/fixtures/bin/utils/files/nested.json +6 -0
- package/test/fixtures/bin/utils/files/no-json-extension.actorrc +1 -0
- package/test/fixtures/bin/utils/files/null.json +1 -0
- package/test/fixtures/bin/utils/files/number.json +1 -0
- package/test/fixtures/bin/utils/files/unreadable.json +1 -0
- package/test/tsconfig.json +4 -2
- package/test/unit/bin/{utils.test.ts → utils/actor-config.test.ts} +152 -52
- package/test/unit/bin/utils/files.test.ts +158 -0
- package/test/unit/lib.test.ts +53 -0
- package/dist/test/unit/bin/utils.test.d.ts +0 -2
- package/dist/test/unit/bin/utils.test.d.ts.map +0 -1
- package/dist/test/unit/bin/utils.test.js.map +0 -1
package/lib/consts.ts
CHANGED
|
@@ -17,6 +17,10 @@ export const TO_FINISH_WITH_OPTIONS: ToFinishWithOptionsWithDefaults = {
|
|
|
17
17
|
*/
|
|
18
18
|
export const DEFAULT_TEST_RUN_DURATION_MS = 60 * 60 * 1000; // 1 hour
|
|
19
19
|
|
|
20
|
+
// Prevent orphaned runs - timeout the Actor 1 minute before the test does so everything is logged correctly.
|
|
21
|
+
// - Otherwise the run link URL can get lost.
|
|
22
|
+
export const DEFAULT_TEST_ACTOR_TIMEOUT_SECS = DEFAULT_TEST_RUN_DURATION_MS / 1000 - 60;
|
|
23
|
+
|
|
20
24
|
/**
|
|
21
25
|
* Delay before checking the dataset and statistics after a run finishes to resolve eventual consistency.
|
|
22
26
|
* - This value should ensure that the dataset and statistics are fully updated before any assertions are made.
|
package/lib/lib.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ApifyClient } from 'apify-client';
|
|
|
3
3
|
import type { SuiteFactory, TestContext, TestFunction } from 'vitest';
|
|
4
4
|
import { describe as vitestDescribe, ExpectStatic, test as vitestTest } from 'vitest';
|
|
5
5
|
|
|
6
|
-
import { DATASET_SYNC_DELAY_MS, DEFAULT_TEST_RUN_DURATION_MS } from './consts.js';
|
|
6
|
+
import { DATASET_SYNC_DELAY_MS, DEFAULT_TEST_ACTOR_TIMEOUT_SECS, DEFAULT_TEST_RUN_DURATION_MS } from './consts.js';
|
|
7
7
|
import { extendExpect } from './extend-expect.js';
|
|
8
8
|
import { RunTestResult } from './run-test-result.js';
|
|
9
9
|
import type { ActorBuild, ActorTestOptions, RunOptions } from './types.js';
|
|
@@ -39,6 +39,7 @@ const DEFAULT_TEST_OPTIONS: ActorTestOptions = {
|
|
|
39
39
|
concurrent: true,
|
|
40
40
|
// test should finish within 1 hour
|
|
41
41
|
timeout: DEFAULT_TEST_RUN_DURATION_MS,
|
|
42
|
+
retry: 1,
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
/**
|
|
@@ -51,12 +52,6 @@ export const describe = (name: string, fn?: SuiteFactory<object>, options: Actor
|
|
|
51
52
|
vitestDescribe.runIf(!!TESTER_APIFY_TOKEN || !!RUN_ALL_PLATFORM_TESTS)(name, options, fn);
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
const DEFAULT_TEST_ACTOR_OPTIONS: ActorTestOptions = {
|
|
55
|
-
retry: 1,
|
|
56
|
-
// prevent orphaned runs
|
|
57
|
-
timeout: DEFAULT_TEST_RUN_DURATION_MS,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
55
|
/**
|
|
61
56
|
* @param actorId - The actor's raw platform ID or its full name (`owner/name`, e.g. `"apify/web-scraper"`).
|
|
62
57
|
*/
|
|
@@ -66,10 +61,8 @@ export const testActor = <T>(
|
|
|
66
61
|
fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
|
|
67
62
|
testOptions?: ActorTestOptions,
|
|
68
63
|
) => {
|
|
69
|
-
const options = {
|
|
70
|
-
|
|
71
|
-
...testOptions,
|
|
72
|
-
};
|
|
64
|
+
const options = { ...DEFAULT_TEST_OPTIONS, ...testOptions };
|
|
65
|
+
|
|
73
66
|
const name = `${actorId}: ${testName}`;
|
|
74
67
|
// `RUN_ALL_PLATFORM_TESTS` is needed for the scheduled tests, which have no `ACTOR_BUILDS` to match the
|
|
75
68
|
// tests against - without it, every test would be filtered out as an actor we didn't build.
|
|
@@ -100,10 +93,8 @@ export const testStandbyActor = <I = any, O = any>(
|
|
|
100
93
|
fn: TestFunction<{ callStandby: ReturnType<typeof createStartStandbyFn<I, O>> }>,
|
|
101
94
|
testOptions?: ActorTestOptions,
|
|
102
95
|
) => {
|
|
103
|
-
const options = {
|
|
104
|
-
|
|
105
|
-
...testOptions,
|
|
106
|
-
};
|
|
96
|
+
const options = { ...DEFAULT_TEST_OPTIONS, ...testOptions };
|
|
97
|
+
|
|
107
98
|
const name = `${actorId}: ${testName}`;
|
|
108
99
|
// `RUN_ALL_PLATFORM_TESTS` is needed for the scheduled tests, which have no `ACTOR_BUILDS` to match the
|
|
109
100
|
// tests against - without it, every test would be filtered out as an actor we didn't build.
|
|
@@ -279,13 +270,20 @@ const createStartRunFn = <T>(actorId: string, testContext: TestContext) => {
|
|
|
279
270
|
return new RunTestResult(apifyClient, run);
|
|
280
271
|
}
|
|
281
272
|
|
|
282
|
-
const actor = apifyClient.actor(actorId);
|
|
283
|
-
|
|
284
273
|
const actorInput = {
|
|
285
274
|
...(prefilledInput && (await getActorPrefilledInput(apifyClient, actorId, buildId))),
|
|
286
275
|
...input,
|
|
287
276
|
};
|
|
288
|
-
|
|
277
|
+
|
|
278
|
+
const actor = apifyClient.actor(actorId);
|
|
279
|
+
const actorInfo = await actor.get();
|
|
280
|
+
const timeout = Math.min(
|
|
281
|
+
actorInfo?.defaultRunOptions?.timeoutSecs ?? DEFAULT_TEST_ACTOR_TIMEOUT_SECS,
|
|
282
|
+
DEFAULT_TEST_ACTOR_TIMEOUT_SECS,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const actorOptions = { timeout, build, log: null, ...options };
|
|
286
|
+
const run = await actor.call(actorInput, actorOptions);
|
|
289
287
|
|
|
290
288
|
const runLink = generateRunLink(run);
|
|
291
289
|
await annotate(`${task.name} - ${runLink}`, 'run_link');
|
|
@@ -306,3 +304,7 @@ const createStartRunFn = <T>(actorId: string, testContext: TestContext) => {
|
|
|
306
304
|
const generateRunLink = (run: ActorRun | ActorRunListItem): string => {
|
|
307
305
|
return `https://console.apify.com/view/runs/${run.id}`;
|
|
308
306
|
};
|
|
307
|
+
|
|
308
|
+
/** Used for unit testing */
|
|
309
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
310
|
+
export const _private = { createStartRunFn } as const;
|
package/lib/types.ts
CHANGED
|
@@ -10,7 +10,15 @@ export type ActorBuild = {
|
|
|
10
10
|
|
|
11
11
|
export type RunOptions<T> = {
|
|
12
12
|
input: Omit<T, 'actorName'>;
|
|
13
|
-
options?: ActorCallOptions
|
|
13
|
+
options?: Omit<ActorCallOptions, 'timeout'> & {
|
|
14
|
+
/**
|
|
15
|
+
* Timeout for the actor run in seconds. Zero value means there is no timeout.
|
|
16
|
+
* - If `undefined`, the run uses timeout of the default Actor run configuration.
|
|
17
|
+
*
|
|
18
|
+
* @default 3540 // 59 minutes (finish the run before the test)
|
|
19
|
+
*/
|
|
20
|
+
timeout?: number;
|
|
21
|
+
};
|
|
14
22
|
prefilledInput?: boolean;
|
|
15
23
|
/**
|
|
16
24
|
* If you specify `runId`, all the other options will be ignored and this run's data will
|
|
@@ -138,8 +146,8 @@ export type ActorTestOptions = Omit<TestOptions, 'retry' | 'timeout'> & {
|
|
|
138
146
|
// we are just extending the docs here to replace the default value, otherwise it's the exact same
|
|
139
147
|
retry?: TestOptions['retry'];
|
|
140
148
|
/**
|
|
141
|
-
* Timeout for the
|
|
142
|
-
* - If `undefined`, the run uses timeout of the default
|
|
149
|
+
* Timeout for the test in milliseconds. Zero value means there is no timeout.
|
|
150
|
+
* - If `undefined`, the run uses timeout of the default test configuration.
|
|
143
151
|
*
|
|
144
152
|
* @default 60 * 60 * 1000 // 1 hour
|
|
145
153
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apify-test-tools",
|
|
3
|
-
"version": "0.9.1-beta.
|
|
3
|
+
"version": "0.9.1-beta.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TBD",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"@slack/web-api": "^7.9.2",
|
|
21
21
|
"apify-client": "^2.22.2",
|
|
22
22
|
"ignore": "^7.0.5",
|
|
23
|
-
"yargs": "^18.0.0"
|
|
23
|
+
"yargs": "^18.0.0",
|
|
24
|
+
"zod": "^4.6.5"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@apify/eslint-config": "^1.0.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{ "a": 1 }]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "a": 1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "a": 1 }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
null
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
42
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "a": 1 }
|
package/test/tsconfig.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "@apify/tsconfig",
|
|
3
3
|
"compilerOptions": {
|
|
4
|
-
"module": "
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
5
6
|
"target": "ES2022",
|
|
6
7
|
"outDir": "dist",
|
|
7
8
|
"noUnusedLocals": false,
|
|
8
9
|
"noEmit": true,
|
|
10
|
+
"rootDir": "..",
|
|
9
11
|
"lib": ["DOM", "es2021"],
|
|
10
12
|
"skipLibCheck": true
|
|
11
13
|
},
|
|
12
|
-
"include": ["./**/*"]
|
|
14
|
+
"include": ["./**/*", "../bin/**/*", "../lib/**/*"]
|
|
13
15
|
}
|
|
@@ -1,36 +1,102 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CONFIG_FILE_NAME,
|
|
9
|
+
loadActorConfig,
|
|
10
|
+
parseConfigFile,
|
|
11
|
+
readConfigFile,
|
|
12
|
+
resolveRawConfig,
|
|
13
|
+
} from '../../../../bin/utils/config/load-config.js';
|
|
14
|
+
|
|
15
|
+
// `readConfigFile` resolves every path against the process's working directory, so these tests give it
|
|
16
|
+
// a real one: a throwaway repo in a temp dir. Reading actual files rather than a mocked
|
|
17
|
+
// `node:fs/promises` is what makes the path handling (the ".actor/" hop, a dockerContextDir escaping
|
|
18
|
+
// the repo root) worth asserting on — against a mock those assertions only describe the mock.
|
|
19
|
+
let repoDir: string;
|
|
20
|
+
let originalCwd: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
originalCwd = process.cwd();
|
|
24
|
+
// `realpath` because macOS hands out a symlinked temp dir, while `process.cwd()` reports the target.
|
|
25
|
+
repoDir = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'apify-test-tools-config-')));
|
|
26
|
+
process.chdir(repoDir);
|
|
27
|
+
});
|
|
12
28
|
|
|
13
|
-
afterEach(() =>
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
process.chdir(originalCwd);
|
|
31
|
+
await fs.rm(repoDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
14
33
|
|
|
15
34
|
const emptyActorSelection = { actors: [], ignore: [] };
|
|
16
35
|
|
|
17
36
|
const validConfig = (actors: object[]) => JSON.stringify({ actors });
|
|
18
37
|
const actorJson = (fields: Record<string, unknown> = {}) => JSON.stringify(fields);
|
|
19
38
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
const writeFiles = async (files: Record<string, string>) =>
|
|
40
|
+
Promise.all(
|
|
41
|
+
Object.entries(files).map(async ([filePath, contents]) => {
|
|
42
|
+
const absPath = path.join(repoDir, filePath);
|
|
43
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
44
|
+
await fs.writeFile(absPath, contents);
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Stages 2-4 are callable on their own, which is the point: a differently shaped config file only
|
|
49
|
+
// has to reach `resolveRawConfig`'s output to work with everything downstream.
|
|
50
|
+
describe('the parse/resolve/load seam', () => {
|
|
51
|
+
it('parseConfigFile validates the file shape without touching the filesystem', () => {
|
|
52
|
+
const entry = { folder: 'actors/shopify', actorFullName: 'myteam/shopify', tokenEnvVar: 'APIFY_TOKEN' };
|
|
53
|
+
|
|
54
|
+
expect(parseConfigFile({ actors: [entry] })).toEqual({ actors: [entry] });
|
|
55
|
+
expect(() => parseConfigFile({ actors: [{ ...entry, folder: 123 }] })).toThrow(/Invalid "folder"/);
|
|
56
|
+
});
|
|
23
57
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
58
|
+
it('resolveRawConfig is where the repo root becomes "" and trailing slashes go', () => {
|
|
59
|
+
const result = resolveRawConfig({
|
|
60
|
+
actors: [
|
|
61
|
+
{ folder: '.', actorFullName: 'myteam/root', tokenEnvVar: 'APIFY_TOKEN' },
|
|
62
|
+
{
|
|
63
|
+
folder: 'actors/shopify/',
|
|
64
|
+
actorFullName: 'myteam/shopify',
|
|
65
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
66
|
+
overrideActorContext: ['packages/'],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual([
|
|
72
|
+
{ folder: '', actorFullName: 'myteam/root', tokenEnvVar: 'APIFY_TOKEN', overrideActorContext: undefined },
|
|
73
|
+
{
|
|
74
|
+
folder: 'actors/shopify',
|
|
75
|
+
actorFullName: 'myteam/shopify',
|
|
76
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
77
|
+
overrideActorContext: ['packages'],
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
28
80
|
});
|
|
29
|
-
|
|
81
|
+
|
|
82
|
+
it('loadActorConfig resolves data coming from actor.json', async () => {
|
|
83
|
+
await writeFiles({ 'actors/shopify/.actor/actor.json': actorJson({ dockerContextDir: '..' }) });
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
loadActorConfig({ folder: 'actors/shopify', actorFullName: 'myteam/shopify', tokenEnvVar: 'APIFY_TOKEN' }),
|
|
87
|
+
).toEqual({
|
|
88
|
+
actorFullName: 'myteam/shopify',
|
|
89
|
+
folder: 'actors/shopify',
|
|
90
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
91
|
+
dockerContextDir: 'actors/shopify',
|
|
92
|
+
contextPaths: ['actors/shopify'],
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
30
96
|
|
|
31
97
|
describe('readConfigFile', () => {
|
|
32
98
|
it('returns correct ActorConfig[] for a valid config', async () => {
|
|
33
|
-
|
|
99
|
+
await writeFiles({
|
|
34
100
|
[CONFIG_FILE_NAME]: validConfig([
|
|
35
101
|
{
|
|
36
102
|
folder: 'actors/shopify',
|
|
@@ -42,7 +108,6 @@ describe('readConfigFile', () => {
|
|
|
42
108
|
});
|
|
43
109
|
|
|
44
110
|
const result = await readConfigFile(emptyActorSelection);
|
|
45
|
-
expectFileRead('actors/shopify/.actor/actor.json');
|
|
46
111
|
expect(result).toEqual([
|
|
47
112
|
{
|
|
48
113
|
actorFullName: 'myteam/shopify-scraper',
|
|
@@ -55,7 +120,7 @@ describe('readConfigFile', () => {
|
|
|
55
120
|
});
|
|
56
121
|
|
|
57
122
|
it('normalizes folder "." to ""', async () => {
|
|
58
|
-
|
|
123
|
+
await writeFiles({
|
|
59
124
|
[CONFIG_FILE_NAME]: validConfig([
|
|
60
125
|
{ folder: '.', actorFullName: 'apify/my-actor', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
61
126
|
]),
|
|
@@ -64,11 +129,10 @@ describe('readConfigFile', () => {
|
|
|
64
129
|
|
|
65
130
|
const result = await readConfigFile(emptyActorSelection);
|
|
66
131
|
expect(result[0].folder).toBe('');
|
|
67
|
-
expectFileRead('.actor/actor.json');
|
|
68
132
|
});
|
|
69
133
|
|
|
70
134
|
it('defaults dockerContextDir to actor folder when absent from actor.json', async () => {
|
|
71
|
-
|
|
135
|
+
await writeFiles({
|
|
72
136
|
[CONFIG_FILE_NAME]: validConfig([
|
|
73
137
|
{ folder: 'actors/web-scraper', actorFullName: 'apify/web-scraper', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
74
138
|
]),
|
|
@@ -81,7 +145,7 @@ describe('readConfigFile', () => {
|
|
|
81
145
|
});
|
|
82
146
|
|
|
83
147
|
it('resolves dockerContextDir relative to .actor/ folder', async () => {
|
|
84
|
-
|
|
148
|
+
await writeFiles({
|
|
85
149
|
[CONFIG_FILE_NAME]: validConfig([
|
|
86
150
|
{ folder: 'actors/shopify', actorFullName: 'myteam/shopify', tokenEnvVar: 'APIFY_TOKEN' },
|
|
87
151
|
]),
|
|
@@ -92,8 +156,19 @@ describe('readConfigFile', () => {
|
|
|
92
156
|
expect(result[0].dockerContextDir).toBe('');
|
|
93
157
|
});
|
|
94
158
|
|
|
159
|
+
it('throws when dockerContextDir resolves outside the repository root', async () => {
|
|
160
|
+
await writeFiles({
|
|
161
|
+
[CONFIG_FILE_NAME]: validConfig([
|
|
162
|
+
{ folder: 'actors/shopify', actorFullName: 'myteam/shopify', tokenEnvVar: 'APIFY_TOKEN' },
|
|
163
|
+
]),
|
|
164
|
+
'actors/shopify/.actor/actor.json': actorJson({ dockerContextDir: '../../../..' }),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow(/resolves outside the repository root/);
|
|
168
|
+
});
|
|
169
|
+
|
|
95
170
|
it('resolves contextPaths from overrideActorContext', async () => {
|
|
96
|
-
|
|
171
|
+
await writeFiles({
|
|
97
172
|
[CONFIG_FILE_NAME]: validConfig([
|
|
98
173
|
{
|
|
99
174
|
folder: 'actors/shopify',
|
|
@@ -110,7 +185,7 @@ describe('readConfigFile', () => {
|
|
|
110
185
|
});
|
|
111
186
|
|
|
112
187
|
it('handles multiple actors', async () => {
|
|
113
|
-
|
|
188
|
+
await writeFiles({
|
|
114
189
|
[CONFIG_FILE_NAME]: validConfig([
|
|
115
190
|
{ folder: 'actors/web-scraper', actorFullName: 'apify/web-scraper', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
116
191
|
{
|
|
@@ -130,22 +205,26 @@ describe('readConfigFile', () => {
|
|
|
130
205
|
});
|
|
131
206
|
|
|
132
207
|
it('throws when config file is missing', async () => {
|
|
133
|
-
fsMock.readFile.mockRejectedValue(new Error('ENOENT'));
|
|
134
208
|
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('not found');
|
|
135
209
|
});
|
|
136
210
|
|
|
137
211
|
it('throws when config file contains invalid JSON', async () => {
|
|
138
|
-
|
|
212
|
+
await writeFiles({ [CONFIG_FILE_NAME]: '{bad json' });
|
|
139
213
|
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('invalid JSON');
|
|
140
214
|
});
|
|
141
215
|
|
|
142
216
|
it('throws when actors array is missing', async () => {
|
|
143
|
-
|
|
217
|
+
await writeFiles({ [CONFIG_FILE_NAME]: JSON.stringify({ notActors: [] }) });
|
|
218
|
+
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('"actors" array');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('throws when the config file is not a JSON object', async () => {
|
|
222
|
+
await writeFiles({ [CONFIG_FILE_NAME]: JSON.stringify([{ folder: 'actors/shopify' }]) });
|
|
144
223
|
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('"actors" array');
|
|
145
224
|
});
|
|
146
225
|
|
|
147
226
|
it('throws on duplicate folders', async () => {
|
|
148
|
-
|
|
227
|
+
await writeFiles({
|
|
149
228
|
[CONFIG_FILE_NAME]: validConfig([
|
|
150
229
|
{ folder: 'actors/shopify', actorFullName: 'apify/shopify', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
151
230
|
{ folder: 'actors/shopify', actorFullName: 'other/shopify', tokenEnvVar: 'APIFY_TOKEN_OTHER' },
|
|
@@ -157,7 +236,7 @@ describe('readConfigFile', () => {
|
|
|
157
236
|
});
|
|
158
237
|
|
|
159
238
|
it('throws on duplicate folders after normalization ("." and "")', async () => {
|
|
160
|
-
|
|
239
|
+
await writeFiles({
|
|
161
240
|
[CONFIG_FILE_NAME]: validConfig([
|
|
162
241
|
{ folder: '.', actorFullName: 'apify/actor-a', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
163
242
|
{ folder: '', actorFullName: 'other/actor-b', tokenEnvVar: 'APIFY_TOKEN_OTHER' },
|
|
@@ -169,7 +248,7 @@ describe('readConfigFile', () => {
|
|
|
169
248
|
});
|
|
170
249
|
|
|
171
250
|
it('throws when actor.json is missing', async () => {
|
|
172
|
-
|
|
251
|
+
await writeFiles({
|
|
173
252
|
[CONFIG_FILE_NAME]: validConfig([
|
|
174
253
|
{ folder: 'actors/shopify', actorFullName: 'apify/shopify', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
175
254
|
]),
|
|
@@ -179,7 +258,7 @@ describe('readConfigFile', () => {
|
|
|
179
258
|
});
|
|
180
259
|
|
|
181
260
|
it('throws when folder is missing', async () => {
|
|
182
|
-
|
|
261
|
+
await writeFiles({
|
|
183
262
|
[CONFIG_FILE_NAME]: validConfig([{ actorFullName: 'apify/shopify', tokenEnvVar: 'APIFY_TOKEN_APIFY' }]),
|
|
184
263
|
});
|
|
185
264
|
|
|
@@ -187,7 +266,7 @@ describe('readConfigFile', () => {
|
|
|
187
266
|
});
|
|
188
267
|
|
|
189
268
|
it('throws when folder is not a string', async () => {
|
|
190
|
-
|
|
269
|
+
await writeFiles({
|
|
191
270
|
[CONFIG_FILE_NAME]: validConfig([
|
|
192
271
|
{ folder: 123, actorFullName: 'apify/shopify', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
193
272
|
]),
|
|
@@ -197,7 +276,7 @@ describe('readConfigFile', () => {
|
|
|
197
276
|
});
|
|
198
277
|
|
|
199
278
|
it('throws when actorFullName is missing', async () => {
|
|
200
|
-
|
|
279
|
+
await writeFiles({
|
|
201
280
|
[CONFIG_FILE_NAME]: validConfig([{ folder: 'actors/shopify', tokenEnvVar: 'APIFY_TOKEN_APIFY' }]),
|
|
202
281
|
'actors/shopify/.actor/actor.json': actorJson({}),
|
|
203
282
|
});
|
|
@@ -206,7 +285,7 @@ describe('readConfigFile', () => {
|
|
|
206
285
|
});
|
|
207
286
|
|
|
208
287
|
it('throws when actorFullName has no slash', async () => {
|
|
209
|
-
|
|
288
|
+
await writeFiles({
|
|
210
289
|
[CONFIG_FILE_NAME]: validConfig([
|
|
211
290
|
{ folder: 'actors/shopify', actorFullName: 'shopify-scraper', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
212
291
|
]),
|
|
@@ -217,7 +296,7 @@ describe('readConfigFile', () => {
|
|
|
217
296
|
});
|
|
218
297
|
|
|
219
298
|
it('throws when actorFullName has empty parts', async () => {
|
|
220
|
-
|
|
299
|
+
await writeFiles({
|
|
221
300
|
[CONFIG_FILE_NAME]: validConfig([
|
|
222
301
|
{ folder: 'actors/shopify', actorFullName: '/shopify', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
223
302
|
]),
|
|
@@ -227,8 +306,29 @@ describe('readConfigFile', () => {
|
|
|
227
306
|
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('Invalid "actorFullName"');
|
|
228
307
|
});
|
|
229
308
|
|
|
309
|
+
it('throws when tokenEnvVar is missing', async () => {
|
|
310
|
+
await writeFiles({
|
|
311
|
+
[CONFIG_FILE_NAME]: validConfig([{ folder: 'actors/shopify', actorFullName: 'apify/shopify' }]),
|
|
312
|
+
'actors/shopify/.actor/actor.json': actorJson({}),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('Invalid "tokenEnvVar"');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('reports every invalid field at once instead of only the first', async () => {
|
|
319
|
+
await writeFiles({
|
|
320
|
+
[CONFIG_FILE_NAME]: validConfig([{ folder: 123, actorFullName: 'shopify', overrideActorContext: 'nope' }]),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const promise = readConfigFile(emptyActorSelection);
|
|
324
|
+
await expect(promise).rejects.toThrow(/Invalid "folder"/);
|
|
325
|
+
await expect(promise).rejects.toThrow(/Invalid "actorFullName"/);
|
|
326
|
+
await expect(promise).rejects.toThrow(/Invalid "tokenEnvVar"/);
|
|
327
|
+
await expect(promise).rejects.toThrow(/Invalid "overrideActorContext"/);
|
|
328
|
+
});
|
|
329
|
+
|
|
230
330
|
it('throws when overrideActorContext is not an array', async () => {
|
|
231
|
-
|
|
331
|
+
await writeFiles({
|
|
232
332
|
[CONFIG_FILE_NAME]: validConfig([
|
|
233
333
|
{
|
|
234
334
|
folder: 'actors/shopify',
|
|
@@ -244,7 +344,7 @@ describe('readConfigFile', () => {
|
|
|
244
344
|
});
|
|
245
345
|
|
|
246
346
|
it('throws when overrideActorContext contains non-strings', async () => {
|
|
247
|
-
|
|
347
|
+
await writeFiles({
|
|
248
348
|
[CONFIG_FILE_NAME]: validConfig([
|
|
249
349
|
{
|
|
250
350
|
folder: 'actors/shopify',
|
|
@@ -260,7 +360,7 @@ describe('readConfigFile', () => {
|
|
|
260
360
|
});
|
|
261
361
|
|
|
262
362
|
it('throws when overrideActorContext entries overlap (one is a prefix of another)', async () => {
|
|
263
|
-
|
|
363
|
+
await writeFiles({
|
|
264
364
|
[CONFIG_FILE_NAME]: validConfig([
|
|
265
365
|
{
|
|
266
366
|
folder: 'actors/shopify',
|
|
@@ -276,7 +376,7 @@ describe('readConfigFile', () => {
|
|
|
276
376
|
});
|
|
277
377
|
|
|
278
378
|
it('throws when overrideActorContext contains the repo root alongside another entry', async () => {
|
|
279
|
-
|
|
379
|
+
await writeFiles({
|
|
280
380
|
[CONFIG_FILE_NAME]: validConfig([
|
|
281
381
|
{
|
|
282
382
|
folder: 'actors/shopify',
|
|
@@ -292,7 +392,7 @@ describe('readConfigFile', () => {
|
|
|
292
392
|
});
|
|
293
393
|
|
|
294
394
|
it('adds the actor own folder automatically when overrideActorContext does not cover it', async () => {
|
|
295
|
-
|
|
395
|
+
await writeFiles({
|
|
296
396
|
[CONFIG_FILE_NAME]: validConfig([
|
|
297
397
|
{
|
|
298
398
|
folder: 'actors/shopify',
|
|
@@ -309,7 +409,7 @@ describe('readConfigFile', () => {
|
|
|
309
409
|
});
|
|
310
410
|
|
|
311
411
|
it('strips trailing slashes from folder and overrideActorContext entries', async () => {
|
|
312
|
-
|
|
412
|
+
await writeFiles({
|
|
313
413
|
[CONFIG_FILE_NAME]: validConfig([
|
|
314
414
|
{
|
|
315
415
|
folder: 'actors/shopify/',
|
|
@@ -327,7 +427,7 @@ describe('readConfigFile', () => {
|
|
|
327
427
|
});
|
|
328
428
|
|
|
329
429
|
it('allows overrideActorContext with disjoint sibling paths that all reach the actor folder via one entry', async () => {
|
|
330
|
-
|
|
430
|
+
await writeFiles({
|
|
331
431
|
[CONFIG_FILE_NAME]: validConfig([
|
|
332
432
|
{
|
|
333
433
|
folder: 'actors/shopify',
|
|
@@ -344,8 +444,8 @@ describe('readConfigFile', () => {
|
|
|
344
444
|
});
|
|
345
445
|
|
|
346
446
|
describe('actor selection', () => {
|
|
347
|
-
const twoActors = () =>
|
|
348
|
-
|
|
447
|
+
const twoActors = async () =>
|
|
448
|
+
writeFiles({
|
|
349
449
|
[CONFIG_FILE_NAME]: validConfig([
|
|
350
450
|
{ folder: 'actors/a', actorFullName: 'team/a', tokenEnvVar: 'TOKEN' },
|
|
351
451
|
{ folder: 'actors/b', actorFullName: 'team/b', tokenEnvVar: 'TOKEN' },
|
|
@@ -357,22 +457,22 @@ describe('readConfigFile', () => {
|
|
|
357
457
|
const fullNames = (result: { actorFullName: string }[]) => result.map((c) => c.actorFullName);
|
|
358
458
|
|
|
359
459
|
it('returns all actors when the selection is empty', async () => {
|
|
360
|
-
twoActors();
|
|
460
|
+
await twoActors();
|
|
361
461
|
expect(fullNames(await readConfigFile(emptyActorSelection))).toEqual(['team/a', 'team/b']);
|
|
362
462
|
});
|
|
363
463
|
|
|
364
464
|
it('keeps only the selected actors', async () => {
|
|
365
|
-
twoActors();
|
|
465
|
+
await twoActors();
|
|
366
466
|
expect(fullNames(await readConfigFile({ actors: ['team/a'], ignore: [] }))).toEqual(['team/a']);
|
|
367
467
|
});
|
|
368
468
|
|
|
369
469
|
it('drops ignored actors', async () => {
|
|
370
|
-
twoActors();
|
|
470
|
+
await twoActors();
|
|
371
471
|
expect(fullNames(await readConfigFile({ actors: [], ignore: ['team/a'] }))).toEqual(['team/b']);
|
|
372
472
|
});
|
|
373
473
|
|
|
374
474
|
it('throws on an unknown actor name', async () => {
|
|
375
|
-
twoActors();
|
|
475
|
+
await twoActors();
|
|
376
476
|
await expect(readConfigFile({ actors: ['team/nope'], ignore: [] })).rejects.toThrow(
|
|
377
477
|
'do not exist: team/nope',
|
|
378
478
|
);
|