apify-test-tools 0.9.1-beta.4 → 0.9.1-beta.6
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/.github/workflows/on_pull_request.yaml +1 -1
- 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 +133 -0
- package/bin/utils/config/parser.ts +88 -0
- package/bin/utils/config/structures/base.ts +61 -0
- package/bin/utils/config/structures/legacy.ts +20 -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 +13 -0
- package/dist/bin/utils/config/load-config.d.ts.map +1 -0
- package/dist/bin/utils/config/load-config.js +99 -0
- package/dist/bin/utils/config/load-config.js.map +1 -0
- package/dist/bin/utils/config/parser.d.ts +19 -0
- package/dist/bin/utils/config/parser.d.ts.map +1 -0
- package/dist/bin/utils/config/parser.js +72 -0
- package/dist/bin/utils/config/parser.js.map +1 -0
- package/dist/bin/utils/config/structures/base.d.ts +35 -0
- package/dist/bin/utils/config/structures/base.d.ts.map +1 -0
- package/dist/bin/utils/config/structures/base.js +40 -0
- package/dist/bin/utils/config/structures/base.js.map +1 -0
- package/dist/bin/utils/config/structures/legacy.d.ts +6 -0
- package/dist/bin/utils/config/structures/legacy.d.ts.map +1 -0
- package/dist/bin/utils/config/structures/legacy.js +14 -0
- package/dist/bin/utils/config/structures/legacy.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/test/unit/bin/utils/config/load-config.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/config/load-config.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js +138 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/parser.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/config/parser.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/config/parser.test.js +111 -0
- package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/structures/base.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/config/structures/base.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/config/structures/base.test.js +74 -0
- package/dist/test/unit/bin/utils/config/structures/base.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/structures/legacy.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/config/structures/legacy.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/config/structures/legacy.test.js +63 -0
- package/dist/test/unit/bin/utils/config/structures/legacy.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/tsconfig.tsbuildinfo +1 -1
- 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/config/load-config.test.ts +179 -0
- package/test/unit/bin/utils/config/parser.test.ts +137 -0
- package/test/unit/bin/utils/config/structures/base.test.ts +83 -0
- package/test/unit/bin/utils/config/structures/legacy.test.ts +78 -0
- package/test/unit/bin/utils/files.test.ts +158 -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 +0 -321
- package/dist/test/unit/bin/utils.test.js.map +0 -1
- package/test/unit/bin/utils.test.ts +0 -381
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.6",
|
|
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
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
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 { CONFIG_FILE_NAME, loadActorConfig, readConfigFile } from '../../../../../bin/utils/config/load-config.js';
|
|
8
|
+
import type { ResolvedActorConfig } from '../../../../../bin/utils/config/structures/base.js';
|
|
9
|
+
|
|
10
|
+
let repoDir: string;
|
|
11
|
+
let originalCwd: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
originalCwd = process.cwd();
|
|
15
|
+
// `realpath` because macOS hands out a symlinked temp dir, while `process.cwd()` reports the target.
|
|
16
|
+
repoDir = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'apify-test-tools-config-')));
|
|
17
|
+
process.chdir(repoDir);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
process.chdir(originalCwd);
|
|
22
|
+
await fs.rm(repoDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const emptyActorSelection = { actors: [], ignore: [] };
|
|
26
|
+
|
|
27
|
+
const validConfig = (actors: object[]) => JSON.stringify({ actors });
|
|
28
|
+
const actorJson = (fields: Record<string, unknown> = {}) => JSON.stringify(fields);
|
|
29
|
+
|
|
30
|
+
const writeFiles = async (files: Record<string, string>) =>
|
|
31
|
+
Promise.all(
|
|
32
|
+
Object.entries(files).map(async ([filePath, contents]) => {
|
|
33
|
+
const absPath = path.join(repoDir, filePath);
|
|
34
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
35
|
+
await fs.writeFile(absPath, contents);
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
describe('loadActorConfig', () => {
|
|
40
|
+
const entry = (fields: Partial<ResolvedActorConfig> = {}): ResolvedActorConfig => ({
|
|
41
|
+
folder: 'actors/shopify',
|
|
42
|
+
actorFullName: 'myteam/shopify',
|
|
43
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
44
|
+
...fields,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const writeActorJson = async (fields: Record<string, unknown> = {}) =>
|
|
48
|
+
writeFiles({ 'actors/shopify/.actor/actor.json': actorJson(fields) });
|
|
49
|
+
|
|
50
|
+
it('resolves data coming from actor.json', async () => {
|
|
51
|
+
await writeActorJson({ dockerContextDir: '..' });
|
|
52
|
+
|
|
53
|
+
expect(loadActorConfig(entry())).toEqual({
|
|
54
|
+
actorFullName: 'myteam/shopify',
|
|
55
|
+
folder: 'actors/shopify',
|
|
56
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
57
|
+
dockerContextDir: 'actors/shopify',
|
|
58
|
+
contextPaths: ['actors/shopify'],
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('defaults dockerContextDir to the actor folder when actor.json does not set it', async () => {
|
|
63
|
+
await writeActorJson();
|
|
64
|
+
|
|
65
|
+
expect(loadActorConfig(entry()).dockerContextDir).toBe('actors/shopify');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('resolves dockerContextDir relative to the .actor/ folder, not the actor folder', async () => {
|
|
69
|
+
await writeActorJson({ dockerContextDir: '../../..' });
|
|
70
|
+
|
|
71
|
+
expect(loadActorConfig(entry()).dockerContextDir).toBe('');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('throws when dockerContextDir resolves outside the repository root', async () => {
|
|
75
|
+
await writeActorJson({ dockerContextDir: '../../../..' });
|
|
76
|
+
|
|
77
|
+
expect(() => loadActorConfig(entry())).toThrow(/resolves outside the repository root/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('throws when actor.json is missing', () => {
|
|
81
|
+
expect(() => loadActorConfig(entry())).toThrow('Cannot read');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('resolves contextPaths from overrideActorContext', async () => {
|
|
85
|
+
await writeActorJson({ dockerContextDir: '../../..' });
|
|
86
|
+
|
|
87
|
+
expect(loadActorConfig(entry({ overrideActorContext: ['actors/shopify', 'packages'] })).contextPaths).toEqual([
|
|
88
|
+
'actors/shopify',
|
|
89
|
+
'packages',
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('adds the actor own folder automatically when overrideActorContext does not cover it', async () => {
|
|
94
|
+
await writeActorJson();
|
|
95
|
+
|
|
96
|
+
expect(loadActorConfig(entry({ overrideActorContext: ['code', 'shared'] })).contextPaths).toEqual([
|
|
97
|
+
'code',
|
|
98
|
+
'shared',
|
|
99
|
+
'actors/shopify',
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('allows overrideActorContext with disjoint sibling paths that all reach the actor folder via one entry', async () => {
|
|
104
|
+
await writeActorJson();
|
|
105
|
+
|
|
106
|
+
expect(
|
|
107
|
+
loadActorConfig(entry({ overrideActorContext: ['actors/shopify', 'code', 'shared'] })).contextPaths,
|
|
108
|
+
).toEqual(['actors/shopify', 'code', 'shared']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('throws when overrideActorContext entries overlap (one is a prefix of another)', async () => {
|
|
112
|
+
await writeActorJson();
|
|
113
|
+
|
|
114
|
+
expect(() => loadActorConfig(entry({ overrideActorContext: ['actors/shopify', 'actors'] }))).toThrow(/overlap/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('throws when overrideActorContext contains the repo root alongside another entry', async () => {
|
|
118
|
+
await writeActorJson();
|
|
119
|
+
|
|
120
|
+
expect(() => loadActorConfig(entry({ overrideActorContext: ['', 'actors/shopify'] }))).toThrow(/overlap/);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('readConfigFile', () => {
|
|
125
|
+
// Golden output massaging the whole config reading process
|
|
126
|
+
it("carries the parser's normalization through to ActorConfig[]", async () => {
|
|
127
|
+
await writeFiles({
|
|
128
|
+
[CONFIG_FILE_NAME]: validConfig([
|
|
129
|
+
{ folder: '.', actorFullName: 'apify/root', tokenEnvVar: 'APIFY_TOKEN_APIFY' },
|
|
130
|
+
{
|
|
131
|
+
folder: 'actors/shopify/',
|
|
132
|
+
actorFullName: 'myteam/shopify',
|
|
133
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
134
|
+
overrideActorContext: ['actors/shopify/', 'packages/'],
|
|
135
|
+
},
|
|
136
|
+
]),
|
|
137
|
+
'.actor/actor.json': actorJson({}),
|
|
138
|
+
'actors/shopify/.actor/actor.json': actorJson({}),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(await readConfigFile(emptyActorSelection)).toEqual([
|
|
142
|
+
{
|
|
143
|
+
actorFullName: 'apify/root',
|
|
144
|
+
folder: '',
|
|
145
|
+
tokenEnvVar: 'APIFY_TOKEN_APIFY',
|
|
146
|
+
dockerContextDir: '',
|
|
147
|
+
contextPaths: [''],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
actorFullName: 'myteam/shopify',
|
|
151
|
+
folder: 'actors/shopify',
|
|
152
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
153
|
+
dockerContextDir: 'actors/shopify',
|
|
154
|
+
contextPaths: ['actors/shopify', 'packages'],
|
|
155
|
+
},
|
|
156
|
+
]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('throws when config file is missing', async () => {
|
|
160
|
+
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('not found');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('throws when config file contains invalid JSON', async () => {
|
|
164
|
+
await writeFiles({ [CONFIG_FILE_NAME]: '{bad json' });
|
|
165
|
+
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('invalid JSON');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('throws when the config file is not a JSON object', async () => {
|
|
169
|
+
await writeFiles({ [CONFIG_FILE_NAME]: JSON.stringify([{ folder: 'actors/shopify' }]) });
|
|
170
|
+
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow('"actors" array');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// The wording of a schema failure belongs to the strategy; this only checks that the failure
|
|
174
|
+
// reaches the caller rather than being swallowed on the way out of readConfigFile.
|
|
175
|
+
it('surfaces a schema failure from the parser', async () => {
|
|
176
|
+
await writeFiles({ [CONFIG_FILE_NAME]: JSON.stringify({ notActors: [] }) });
|
|
177
|
+
await expect(readConfigFile(emptyActorSelection)).rejects.toThrow(/at actors/);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { _privates, parseConfigFile } from '../../../../../bin/utils/config/parser.js';
|
|
4
|
+
import { CONFIG_FILE_STRATEGY } from '../../../../../bin/utils/config/structures/base.js';
|
|
5
|
+
import { LEGACY_PARSER } from '../../../../../bin/utils/config/structures/legacy.js';
|
|
6
|
+
|
|
7
|
+
const { selectStrategy, verifyConfiguration } = _privates;
|
|
8
|
+
|
|
9
|
+
const actor = (fields: Record<string, unknown> = {}) => ({
|
|
10
|
+
folder: 'actors/shopify',
|
|
11
|
+
actorFullName: 'myteam/shopify',
|
|
12
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
13
|
+
...fields,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('selectStrategy', () => {
|
|
17
|
+
it('falls back to the legacy strategy when no mode is declared', () => {
|
|
18
|
+
expect(selectStrategy(undefined)).toBe(LEGACY_PARSER);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('honours an explicit mode', () => {
|
|
22
|
+
expect(selectStrategy(CONFIG_FILE_STRATEGY.LEGACY)).toBe(LEGACY_PARSER);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('throws on a mode no strategy is registered for', () => {
|
|
26
|
+
expect(() => selectStrategy('some-nonexistent-mode')).toThrow(/Invalid input/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('verifyConfiguration', () => {
|
|
31
|
+
it('resolves the repo root to "" and gets rid of trailing slashes', () => {
|
|
32
|
+
const result = verifyConfiguration([
|
|
33
|
+
{ folder: '.', actorFullName: 'myteam/root', tokenEnvVar: 'APIFY_TOKEN' },
|
|
34
|
+
{
|
|
35
|
+
folder: 'actors/shopify/',
|
|
36
|
+
actorFullName: 'myteam/shopify',
|
|
37
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
38
|
+
overrideActorContext: ['packages/'],
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
expect(result).toEqual([
|
|
43
|
+
{ folder: '', actorFullName: 'myteam/root', tokenEnvVar: 'APIFY_TOKEN', overrideActorContext: undefined },
|
|
44
|
+
{
|
|
45
|
+
folder: 'actors/shopify',
|
|
46
|
+
actorFullName: 'myteam/shopify',
|
|
47
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
48
|
+
overrideActorContext: ['packages'],
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each([
|
|
54
|
+
['"." and ""', '.', ''],
|
|
55
|
+
['weird roots', '', './//'],
|
|
56
|
+
['a trailing slash', 'actors/shopify', 'actors/shopify/'],
|
|
57
|
+
])('rejects folders that collide once normalized: %s', (_name, first, second) => {
|
|
58
|
+
expect(() =>
|
|
59
|
+
verifyConfiguration([
|
|
60
|
+
actor({ folder: first, actorFullName: 'myteam/actor-a' }),
|
|
61
|
+
actor({ folder: second, actorFullName: 'myteam/actor-b' }),
|
|
62
|
+
]),
|
|
63
|
+
).toThrow(/Duplicate folder/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('quotes a duplicate folder the way it was written, not the way it normalized', () => {
|
|
67
|
+
expect(() =>
|
|
68
|
+
verifyConfiguration([
|
|
69
|
+
actor({ folder: '.', actorFullName: 'myteam/actor-a' }),
|
|
70
|
+
actor({ folder: './', actorFullName: 'myteam/actor-b' }),
|
|
71
|
+
]),
|
|
72
|
+
).toThrow('Duplicate folder "./"');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rejects two entries pointing at the same actor', () => {
|
|
76
|
+
expect(() => verifyConfiguration([actor({ folder: 'actors/a' }), actor({ folder: 'actors/b' })])).toThrow(
|
|
77
|
+
/Duplicate actor "myteam\/shopify"/,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('treats actorFullName case-sensitively — "myteam/A" is not "myteam/a"', () => {
|
|
82
|
+
expect(() =>
|
|
83
|
+
verifyConfiguration([
|
|
84
|
+
actor({ folder: 'actors/a', actorFullName: 'myteam/abc' }),
|
|
85
|
+
actor({ folder: 'actors/b', actorFullName: 'myteam/ABC' }),
|
|
86
|
+
]),
|
|
87
|
+
).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('reports every collision at once instead of stopping at the first', () => {
|
|
91
|
+
const message = (() => {
|
|
92
|
+
try {
|
|
93
|
+
verifyConfiguration([
|
|
94
|
+
actor({ folder: 'actors/a', actorFullName: 'myteam/actor-a' }),
|
|
95
|
+
actor({ folder: 'actors/a', actorFullName: 'myteam/actor-b' }),
|
|
96
|
+
actor({ folder: 'actors/c', actorFullName: 'myteam/actor-c' }),
|
|
97
|
+
actor({ folder: 'actors/d', actorFullName: 'myteam/actor-c' }),
|
|
98
|
+
]);
|
|
99
|
+
return '';
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return (err as Error).message;
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
expect(message.split('\n')).toEqual([
|
|
106
|
+
'Duplicate folder "actors/a". Each actor must have a unique folder.',
|
|
107
|
+
'Duplicate actor "myteam/actor-c". Each entry must point at a different actor.',
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('reports both problems for an entry that duplicates a folder and an actor at once', () => {
|
|
112
|
+
expect(() => verifyConfiguration([actor(), actor()])).toThrow(/Duplicate folder[\s\S]*Duplicate actor/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('accepts an empty configuration', () => {
|
|
116
|
+
expect(verifyConfiguration([])).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('parseConfigFile', () => {
|
|
121
|
+
it('validates and normalizes a plain object without touching the filesystem', () => {
|
|
122
|
+
expect(parseConfigFile({ actors: [actor({ folder: 'actors/shopify/' })] })).toEqual([
|
|
123
|
+
actor({ folder: 'actors/shopify', overrideActorContext: undefined }),
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('surfaces cross-entry violations from verifyConfiguration', () => {
|
|
128
|
+
expect(() =>
|
|
129
|
+
parseConfigFile({
|
|
130
|
+
actors: [
|
|
131
|
+
actor({ actorFullName: 'myteam/actor-a', folder: 'collide/collide' }),
|
|
132
|
+
actor({ actorFullName: 'myteam/actor-b', folder: 'collide/collide' }),
|
|
133
|
+
],
|
|
134
|
+
}),
|
|
135
|
+
).toThrow(/Duplicate folder/);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ACTOR_NAME, USERNAME } from '@apify/consts';
|
|
4
|
+
|
|
5
|
+
import { ACTOR_FULL_NAME_REGEX } from '../../../../../../bin/utils/config/structures/base.js';
|
|
6
|
+
|
|
7
|
+
// ACTOR_FULL_NAME_REGEX is spliced together from two platform constants by stripping their anchors
|
|
8
|
+
// and joining them with a "/". These tests are about the splice, not about re-testing the platform's
|
|
9
|
+
// own rules: an anchor left behind, an anchor stripped too eagerly, or the actor-name alternation
|
|
10
|
+
// losing its parentheses would all produce a regex that still looks plausible and matches the wrong
|
|
11
|
+
// things. The cases below are the ones that tell those failures apart.
|
|
12
|
+
describe('ACTOR_FULL_NAME_REGEX', () => {
|
|
13
|
+
it.each([
|
|
14
|
+
['apify/web-scraper'],
|
|
15
|
+
['myteam/shopify'],
|
|
16
|
+
['my.team/web-scraper'],
|
|
17
|
+
['my-team/shopify'],
|
|
18
|
+
['my_team/a1'],
|
|
19
|
+
// ACTOR_NAME's first alternative — a single character — is a legal actor name on its own.
|
|
20
|
+
['myteam/a'],
|
|
21
|
+
['myteam/a--b'],
|
|
22
|
+
])('accepts %s', (fullName) => {
|
|
23
|
+
expect(ACTOR_FULL_NAME_REGEX.test(fullName)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it.each([
|
|
27
|
+
['there is no slash', 'shopify'],
|
|
28
|
+
['the owner half is empty', '/shopify'],
|
|
29
|
+
['the actor half is empty', 'myteam/'],
|
|
30
|
+
['there are two slashes', 'myteam//shopify'],
|
|
31
|
+
['there is a third segment', 'myteam/shopify/extra'],
|
|
32
|
+
['the owner holds a space', 'my team/shopify'],
|
|
33
|
+
['the actor name starts with a hyphen', 'myteam/-shopify'],
|
|
34
|
+
['the actor name ends with a hyphen', 'myteam/shopify-'],
|
|
35
|
+
['the actor name holds an underscore', 'myteam/web_scraper'],
|
|
36
|
+
['the actor name holds a dot', 'myteam/web.scraper'],
|
|
37
|
+
])('rejects a name where %s', (_name, fullName) => {
|
|
38
|
+
expect(ACTOR_FULL_NAME_REGEX.test(fullName)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Both halves arrive anchored and get de-anchored so they can be joined; the composed regex has to
|
|
42
|
+
// put the anchors back. Without them these all match on a substring.
|
|
43
|
+
it.each([
|
|
44
|
+
['leading text', 'see myteam/shopify'],
|
|
45
|
+
['trailing text', 'myteam/shopify here'],
|
|
46
|
+
['a leading newline', '\nmyteam/shopify'],
|
|
47
|
+
['a trailing newline', 'myteam/shopify\n'],
|
|
48
|
+
['surrounding whitespace', ' myteam/shopify '],
|
|
49
|
+
])('matches the whole string, so it rejects %s', (_name, fullName) => {
|
|
50
|
+
expect(ACTOR_FULL_NAME_REGEX.test(fullName)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ACTOR_NAME's source is an alternation, "(A|B)". It survives the splice only because the platform
|
|
54
|
+
// wrote it parenthesized — lose those parens and the regex becomes "^owner/A|B$", where the "B"
|
|
55
|
+
// branch matches a bare actor name with no owner at all.
|
|
56
|
+
it('keeps the actor-name alternation from swallowing the owner half', () => {
|
|
57
|
+
expect(ACTOR_NAME.REGEX.source).toMatch(/^\^\(.*\)\$$/);
|
|
58
|
+
expect(ACTOR_FULL_NAME_REGEX.test('shopify')).toBe(false);
|
|
59
|
+
expect(ACTOR_FULL_NAME_REGEX.test('a')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// The owner half carries a {min,max} quantifier, the one part of either source that a sloppy strip
|
|
63
|
+
// could eat. Bounds come from the constants so this stays honest if the platform moves them.
|
|
64
|
+
it("enforces the platform's username length bounds", () => {
|
|
65
|
+
const owner = (length: number) => `${'a'.repeat(length)}/shopify`;
|
|
66
|
+
|
|
67
|
+
expect(ACTOR_FULL_NAME_REGEX.test(owner(USERNAME.MIN_LENGTH))).toBe(true);
|
|
68
|
+
expect(ACTOR_FULL_NAME_REGEX.test(owner(USERNAME.MIN_LENGTH - 1))).toBe(false);
|
|
69
|
+
expect(ACTOR_FULL_NAME_REGEX.test(owner(USERNAME.MAX_LENGTH))).toBe(true);
|
|
70
|
+
expect(ACTOR_FULL_NAME_REGEX.test(owner(USERNAME.MAX_LENGTH + 1))).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// `.source` carries the pattern and none of the flags, so the flags have to be re-applied by hand.
|
|
74
|
+
// USERNAME.REGEX is declared /i and the platform does accept "MyTeam" as a username; drop that on
|
|
75
|
+
// the way here and the owner half silently becomes stricter than the thing it was copied from.
|
|
76
|
+
it('accepts either half in any case, the way the platform does', () => {
|
|
77
|
+
expect(USERNAME.REGEX.flags).toContain('i');
|
|
78
|
+
expect(ACTOR_FULL_NAME_REGEX.flags).toContain('i');
|
|
79
|
+
|
|
80
|
+
expect(ACTOR_FULL_NAME_REGEX.test('myteam/Shopify')).toBe(true);
|
|
81
|
+
expect(ACTOR_FULL_NAME_REGEX.test('MyTeam/shopify')).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { LEGACY_PARSER } from '../../../../../../bin/utils/config/structures/legacy.js';
|
|
4
|
+
|
|
5
|
+
const entry = (fields: Record<string, unknown> = {}) => ({
|
|
6
|
+
folder: 'actors/shopify',
|
|
7
|
+
actorFullName: 'myteam/shopify',
|
|
8
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
9
|
+
...fields,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('LEGACY_PARSER', () => {
|
|
13
|
+
it("returns the entries as written — normalizing is not this layer's job", () => {
|
|
14
|
+
const raw = entry({ folder: 'actors/shopify/', overrideActorContext: ['packages/'] });
|
|
15
|
+
|
|
16
|
+
expect(LEGACY_PARSER.parse({ actors: [raw] })).toEqual([raw]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('drops the "mode" discriminator rather than choking on it', () => {
|
|
20
|
+
expect(LEGACY_PARSER.parse({ mode: 'legacy', actors: [entry()] })).toEqual([entry()]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects a file with no "actors" array', () => {
|
|
24
|
+
expect(() => LEGACY_PARSER.parse({ notActors: [] })).toThrow(/expected array[\s\S]*at actors/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects an empty "actors" array', () => {
|
|
28
|
+
expect(() => LEGACY_PARSER.parse({ actors: [] })).toThrow(/Too small/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each([
|
|
32
|
+
['folder is missing', { folder: undefined }, /at actors\[0\]\.folder/],
|
|
33
|
+
['folder is not a string', { folder: 123 }, /at actors\[0\]\.folder/],
|
|
34
|
+
['tokenEnvVar is missing', { tokenEnvVar: undefined }, /at actors\[0\]\.tokenEnvVar/],
|
|
35
|
+
['overrideActorContext is not an array', { overrideActorContext: 'packages' }, /expected array/],
|
|
36
|
+
['overrideActorContext holds non-strings', { overrideActorContext: [123] }, /overrideActorContext\[0\]/],
|
|
37
|
+
])('rejects an entry where %s', (_name, fields, expected) => {
|
|
38
|
+
expect(() => LEGACY_PARSER.parse({ actors: [entry(fields)] })).toThrow(expected);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// The "owner/name" halves are checked against the platform's own username/actor-name rules.
|
|
42
|
+
it.each([
|
|
43
|
+
['no slash', 'shopify-scraper'],
|
|
44
|
+
['an empty owner', '/shopify'],
|
|
45
|
+
['an empty name', 'myteam/'],
|
|
46
|
+
['more than two halves', 'myteam/shopify/extra'],
|
|
47
|
+
])('rejects an actorFullName with %s', (_name, actorFullName) => {
|
|
48
|
+
expect(() => LEGACY_PARSER.parse({ actors: [entry({ actorFullName })] })).toThrow(
|
|
49
|
+
/at actors\[0\]\.actorFullName/,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Uppercase owners are legal on the platform; ./base.test.ts covers why that is easy to lose.
|
|
54
|
+
it.each([['myteam/shopify'], ['my.team/web-scraper'], ['my_team/a1'], ['apify/web-scraper'], ['MyTeam/shopify']])(
|
|
55
|
+
'accepts the actorFullName %s',
|
|
56
|
+
(actorFullName) => {
|
|
57
|
+
expect(() => LEGACY_PARSER.parse({ actors: [entry({ actorFullName })] })).not.toThrow();
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
it('reports every invalid field at once instead of only the first', () => {
|
|
62
|
+
const message = (() => {
|
|
63
|
+
try {
|
|
64
|
+
LEGACY_PARSER.parse({
|
|
65
|
+
actors: [{ folder: 123, actorFullName: 'shopify', overrideActorContext: 'nope' }],
|
|
66
|
+
});
|
|
67
|
+
throw new Error('Function should have thrown');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return (err as Error).message;
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
|
|
73
|
+
expect(message).toContain('actors[0].folder');
|
|
74
|
+
expect(message).toContain('actors[0].actorFullName');
|
|
75
|
+
expect(message).toContain('actors[0].tokenEnvVar');
|
|
76
|
+
expect(message).toContain('actors[0].overrideActorContext');
|
|
77
|
+
});
|
|
78
|
+
});
|