apify-test-tools 0.10.1-beta.2 → 0.10.1-beta.4
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/actions/checkout-restore-dependencies/action.yaml +4 -3
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +7 -2
- package/.github/workflows/public_platform-tests.yaml +0 -1
- package/.github/workflows-package-version +1 -1
- package/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +13 -3
- package/README.md +2 -2
- package/bin/build-from-local.ts +8 -7
- package/bin/build.ts +22 -21
- package/bin/diff-changes.ts +11 -10
- package/bin/diff-json-schema.ts +14 -12
- package/bin/git.ts +89 -47
- package/bin/logger.ts +23 -0
- package/bin/main.ts +44 -38
- package/bin/output.ts +6 -0
- package/bin/slack.ts +8 -7
- package/bin/test-report.ts +22 -21
- package/bin/types.ts +0 -1
- package/bin/utils/config/actor-json.ts +84 -0
- package/bin/utils/config/load-config.ts +46 -46
- package/bin/utils/config/parser.ts +33 -28
- package/bin/utils/config/structures/base.ts +7 -0
- package/bin/utils/json-file.ts +11 -14
- package/bin/utils/path/repo-relative.ts +156 -0
- package/bin/utils/path/schema.ts +18 -0
- package/bin/utils.ts +63 -44
- package/dist/bin/build-from-local.d.ts +1 -1
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +8 -7
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +22 -21
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +1 -1
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +9 -8
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +1 -1
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +13 -12
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +10 -10
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +78 -37
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/logger.d.ts +11 -0
- package/dist/bin/logger.d.ts.map +1 -0
- package/dist/bin/logger.js +20 -0
- package/dist/bin/logger.js.map +1 -0
- package/dist/bin/main.d.ts +1 -1
- package/dist/bin/main.js +44 -38
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/output.d.ts +3 -0
- package/dist/bin/output.d.ts.map +1 -0
- package/dist/bin/output.js +6 -0
- package/dist/bin/output.js.map +1 -0
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +8 -7
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +22 -21
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +0 -1
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils/config/actor-json.d.ts +14 -0
- package/dist/bin/utils/config/actor-json.d.ts.map +1 -0
- package/dist/bin/utils/config/actor-json.js +60 -0
- package/dist/bin/utils/config/actor-json.js.map +1 -0
- package/dist/bin/utils/config/load-config.d.ts +12 -2
- package/dist/bin/utils/config/load-config.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.js +32 -31
- package/dist/bin/utils/config/load-config.js.map +1 -1
- package/dist/bin/utils/config/parser.d.ts +4 -12
- package/dist/bin/utils/config/parser.d.ts.map +1 -1
- package/dist/bin/utils/config/parser.js +24 -23
- package/dist/bin/utils/config/parser.js.map +1 -1
- package/dist/bin/utils/config/structures/base.d.ts +5 -0
- package/dist/bin/utils/config/structures/base.d.ts.map +1 -1
- package/dist/bin/utils/config/structures/base.js.map +1 -1
- package/dist/bin/utils/json-file.d.ts +3 -2
- package/dist/bin/utils/json-file.d.ts.map +1 -1
- package/dist/bin/utils/json-file.js +8 -13
- package/dist/bin/utils/json-file.js.map +1 -1
- package/dist/bin/utils/path/repo-relative.d.ts +35 -0
- package/dist/bin/utils/path/repo-relative.d.ts.map +1 -0
- package/dist/bin/utils/path/repo-relative.js +137 -0
- package/dist/bin/utils/path/repo-relative.js.map +1 -0
- package/dist/bin/utils/path/schema.d.ts +7 -0
- package/dist/bin/utils/path/schema.d.ts.map +1 -0
- package/dist/bin/utils/path/schema.js +16 -0
- package/dist/bin/utils/path/schema.js.map +1 -0
- package/dist/bin/utils.d.ts +14 -6
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +52 -36
- package/dist/bin/utils.js.map +1 -1
- package/dist/test/unit/bin/build-from-local.test.js +4 -40
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +97 -96
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +52 -48
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git-command.test.d.ts +2 -0
- package/dist/test/unit/bin/git-command.test.d.ts.map +1 -0
- package/dist/test/unit/bin/git-command.test.js +54 -0
- package/dist/test/unit/bin/git-command.test.js.map +1 -0
- package/dist/test/unit/bin/git.test.js +211 -94
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/logger.test.d.ts +2 -0
- package/dist/test/unit/bin/logger.test.d.ts.map +1 -0
- package/dist/test/unit/bin/logger.test.js +44 -0
- package/dist/test/unit/bin/logger.test.js.map +1 -0
- package/dist/test/unit/bin/logging.test.d.ts +2 -0
- package/dist/test/unit/bin/logging.test.d.ts.map +1 -0
- package/dist/test/unit/bin/logging.test.js +43 -0
- package/dist/test/unit/bin/logging.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js +47 -25
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/parser.test.js +64 -19
- package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
- package/dist/test/unit/bin/utils/files.test.js +16 -27
- package/dist/test/unit/bin/utils/files.test.js.map +1 -1
- package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js +196 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js.map +1 -0
- package/dist/test/unit/bin/utils/path/schema.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/path/schema.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/path/schema.test.js +52 -0
- package/dist/test/unit/bin/utils/path/schema.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +40 -38
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eslint.config.mjs +9 -1
- package/package.json +5 -5
- package/test/unit/bin/build-from-local.test.ts +4 -54
- package/test/unit/bin/diff-changes.test.ts +97 -96
- package/test/unit/bin/diff.test.ts +53 -48
- package/test/unit/bin/git-command.test.ts +71 -0
- package/test/unit/bin/git.test.ts +246 -91
- package/test/unit/bin/logger.test.ts +55 -0
- package/test/unit/bin/logging.test.ts +52 -0
- package/test/unit/bin/utils/config/load-config.test.ts +63 -26
- package/test/unit/bin/utils/config/parser.test.ts +81 -27
- package/test/unit/bin/utils/files.test.ts +19 -33
- package/test/unit/bin/utils/path/repo-relative.test.ts +250 -0
- package/test/unit/bin/utils/path/schema.test.ts +65 -0
- package/test/unit/should-built-and-test.test.ts +40 -38
|
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
6
6
|
|
|
7
7
|
import { CONFIG_FILE_NAME, loadActorConfig, readConfigFile } from '../../../../../bin/utils/config/load-config.js';
|
|
8
8
|
import type { ResolvedActorConfig } from '../../../../../bin/utils/config/structures/base.js';
|
|
9
|
+
import { ExistingDir, ExistingFile, RelativeDir, RelativeFile } from '../../../../../bin/utils/path/repo-relative.js';
|
|
9
10
|
|
|
10
11
|
let repoDir: string;
|
|
11
12
|
let originalCwd: string;
|
|
@@ -15,6 +16,7 @@ beforeEach(async () => {
|
|
|
15
16
|
// `realpath` because macOS hands out a symlinked temp dir, while `process.cwd()` reports the target.
|
|
16
17
|
repoDir = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'apify-test-tools-config-')));
|
|
17
18
|
process.chdir(repoDir);
|
|
19
|
+
await fs.mkdir('actors/shopify', { recursive: true });
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
afterEach(async () => {
|
|
@@ -37,12 +39,11 @@ const writeFiles = async (files: Record<string, string>) =>
|
|
|
37
39
|
);
|
|
38
40
|
|
|
39
41
|
describe('loadActorConfig', () => {
|
|
40
|
-
const entry = (fields: Partial<ResolvedActorConfig> = {})
|
|
41
|
-
folder: 'actors/shopify',
|
|
42
|
-
actorFullName: 'myteam/shopify',
|
|
43
|
-
tokenEnvVar: 'APIFY_TOKEN',
|
|
44
|
-
overrideActorContext:
|
|
45
|
-
...fields,
|
|
42
|
+
const entry = (fields: Partial<ResolvedActorConfig> = {}) => ({
|
|
43
|
+
folder: new ExistingDir(fields.folder ?? 'actors/shopify'),
|
|
44
|
+
actorFullName: fields.actorFullName ?? 'myteam/shopify',
|
|
45
|
+
tokenEnvVar: fields.tokenEnvVar ?? 'APIFY_TOKEN',
|
|
46
|
+
overrideActorContext: fields.overrideActorContext?.map((contextPath) => new RelativeDir(contextPath)),
|
|
46
47
|
});
|
|
47
48
|
|
|
48
49
|
const writeActorJson = async (fields: Record<string, unknown> = {}) =>
|
|
@@ -51,31 +52,64 @@ describe('loadActorConfig', () => {
|
|
|
51
52
|
it('resolves data coming from actor.json', async () => {
|
|
52
53
|
await writeActorJson({ dockerContextDir: '..' });
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const loaded = loadActorConfig(entry());
|
|
56
|
+
expect(loaded.actorFullName).toBe('myteam/shopify');
|
|
57
|
+
expect(loaded.folder).toBeInstanceOf(ExistingDir);
|
|
58
|
+
expect(loaded.folder.path).toBe('actors/shopify');
|
|
59
|
+
expect(loaded.actorJson.file).toBeInstanceOf(ExistingFile);
|
|
60
|
+
expect(loaded.actorJson.file.path).toBe('actors/shopify/.actor/actor.json');
|
|
61
|
+
expect(loaded.dockerContextDir).toBeInstanceOf(RelativeDir);
|
|
62
|
+
expect(loaded.dockerContextDir.path).toBe('actors/shopify');
|
|
63
|
+
expect(loaded.contextPaths.map((contextPath) => contextPath.path)).toEqual(['actors/shopify']);
|
|
61
64
|
});
|
|
62
65
|
|
|
63
66
|
it('defaults dockerContextDir to the actor folder when actor.json does not set it', async () => {
|
|
64
67
|
await writeActorJson();
|
|
65
68
|
|
|
66
|
-
expect(loadActorConfig(entry()).dockerContextDir).toBe('actors/shopify');
|
|
69
|
+
expect(loadActorConfig(entry()).dockerContextDir.path).toBe('actors/shopify');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('resolves path fields from actor.json while allowing inline schemas', async () => {
|
|
73
|
+
await writeActorJson({
|
|
74
|
+
readme: '../README.md',
|
|
75
|
+
changelog: '../../CHANGELOG.md',
|
|
76
|
+
dockerfile: '../Dockerfile',
|
|
77
|
+
input: '../INPUT_SCHEMA.json',
|
|
78
|
+
output: { type: 'object' },
|
|
79
|
+
webServerSchema: './openapi.json',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const { actorJson: actorJsonPaths } = loadActorConfig(entry());
|
|
83
|
+
expect(actorJsonPaths.readme).toBeInstanceOf(RelativeFile);
|
|
84
|
+
expect(actorJsonPaths.readme?.path).toBe('actors/shopify/README.md');
|
|
85
|
+
expect(actorJsonPaths.changelog?.path).toBe('actors/CHANGELOG.md');
|
|
86
|
+
expect(actorJsonPaths.dockerfile?.path).toBe('actors/shopify/Dockerfile');
|
|
87
|
+
expect(actorJsonPaths.inputSchema?.path).toBe('actors/shopify/INPUT_SCHEMA.json');
|
|
88
|
+
expect(actorJsonPaths.outputSchema).toBeUndefined();
|
|
89
|
+
expect(actorJsonPaths.webServerSchema?.path).toBe('actors/shopify/.actor/openapi.json');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('rejects malformed actor.json path fields', async () => {
|
|
93
|
+
await writeActorJson({ dockerContextDir: 123 });
|
|
94
|
+
expect(() => loadActorConfig(entry())).toThrow(/dockerContextDir/);
|
|
95
|
+
|
|
96
|
+
await writeActorJson({ readme: '/outside.md' });
|
|
97
|
+
expect(() => loadActorConfig(entry())).toThrow(/Invalid "readme"/);
|
|
98
|
+
|
|
99
|
+
await writeActorJson({ inputSchema: '/outside.json' });
|
|
100
|
+
expect(() => loadActorConfig(entry())).toThrow(/Invalid "inputSchema"/);
|
|
67
101
|
});
|
|
68
102
|
|
|
69
103
|
it('resolves dockerContextDir relative to the .actor/ folder, not the actor folder', async () => {
|
|
70
104
|
await writeActorJson({ dockerContextDir: '../../..' });
|
|
71
105
|
|
|
72
|
-
expect(loadActorConfig(entry()).dockerContextDir).toBe('');
|
|
106
|
+
expect(loadActorConfig(entry()).dockerContextDir.path).toBe('.');
|
|
73
107
|
});
|
|
74
108
|
|
|
75
109
|
it('throws when dockerContextDir resolves outside the repository root', async () => {
|
|
76
110
|
await writeActorJson({ dockerContextDir: '../../../..' });
|
|
77
111
|
|
|
78
|
-
expect(() => loadActorConfig(entry())).toThrow(/
|
|
112
|
+
expect(() => loadActorConfig(entry())).toThrow(/escapes the repo root/);
|
|
79
113
|
});
|
|
80
114
|
|
|
81
115
|
it('throws when actor.json is missing', () => {
|
|
@@ -85,27 +119,30 @@ describe('loadActorConfig', () => {
|
|
|
85
119
|
it('resolves contextPaths from overrideActorContext', async () => {
|
|
86
120
|
await writeActorJson({ dockerContextDir: '../../..' });
|
|
87
121
|
|
|
88
|
-
expect(
|
|
89
|
-
'actors/shopify',
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
expect(
|
|
123
|
+
loadActorConfig(entry({ overrideActorContext: ['actors/shopify', 'packages'] })).contextPaths.map(
|
|
124
|
+
(contextPath) => contextPath.path,
|
|
125
|
+
),
|
|
126
|
+
).toEqual(['actors/shopify', 'packages']);
|
|
92
127
|
});
|
|
93
128
|
|
|
94
129
|
it('adds the actor own folder automatically when overrideActorContext does not cover it', async () => {
|
|
95
130
|
await writeActorJson();
|
|
96
131
|
|
|
97
|
-
expect(
|
|
98
|
-
'code',
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
]);
|
|
132
|
+
expect(
|
|
133
|
+
loadActorConfig(entry({ overrideActorContext: ['code', 'shared'] })).contextPaths.map(
|
|
134
|
+
(contextPath) => contextPath.path,
|
|
135
|
+
),
|
|
136
|
+
).toEqual(['code', 'shared', 'actors/shopify']);
|
|
102
137
|
});
|
|
103
138
|
|
|
104
139
|
it('allows overrideActorContext with disjoint sibling paths that all reach the actor folder via one entry', async () => {
|
|
105
140
|
await writeActorJson();
|
|
106
141
|
|
|
107
142
|
expect(
|
|
108
|
-
loadActorConfig(entry({ overrideActorContext: ['actors/shopify', 'code', 'shared'] })).contextPaths
|
|
143
|
+
loadActorConfig(entry({ overrideActorContext: ['actors/shopify', 'code', 'shared'] })).contextPaths.map(
|
|
144
|
+
(contextPath) => contextPath.path,
|
|
145
|
+
),
|
|
109
146
|
).toEqual(['actors/shopify', 'code', 'shared']);
|
|
110
147
|
});
|
|
111
148
|
|
|
@@ -1,12 +1,43 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
6
|
|
|
3
7
|
import { _privates, parseConfigFile } from '../../../../../bin/utils/config/parser.js';
|
|
4
8
|
import { CONFIG_FILE_STRATEGY } from '../../../../../bin/utils/config/structures/base.js';
|
|
5
9
|
import { GROUPED_PARSER } from '../../../../../bin/utils/config/structures/grouped.js';
|
|
6
10
|
import { LEGACY_PARSER } from '../../../../../bin/utils/config/structures/legacy.js';
|
|
11
|
+
import { ExistingDir, RelativeDir } from '../../../../../bin/utils/path/repo-relative.js';
|
|
7
12
|
|
|
8
13
|
const { selectStrategy, verifyConfiguration } = _privates;
|
|
9
14
|
|
|
15
|
+
let repoDir: string;
|
|
16
|
+
const originalCwd = process.cwd();
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
repoDir = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'apify-test-tools-parser-')));
|
|
20
|
+
for (const folder of [
|
|
21
|
+
'actors/shopify',
|
|
22
|
+
'actors/a',
|
|
23
|
+
'actors/b',
|
|
24
|
+
'actors/c',
|
|
25
|
+
'actors/d',
|
|
26
|
+
'bin',
|
|
27
|
+
'test',
|
|
28
|
+
'collide/collide',
|
|
29
|
+
]) {
|
|
30
|
+
await fs.mkdir(path.join(repoDir, folder), { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
await fs.writeFile(path.join(repoDir, 'package.json'), '{}');
|
|
33
|
+
process.chdir(repoDir);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
process.chdir(originalCwd);
|
|
38
|
+
await fs.rm(repoDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
10
41
|
const actor = (fields: Record<string, unknown> = {}) => ({
|
|
11
42
|
folder: 'actors/shopify',
|
|
12
43
|
actorFullName: 'myteam/shopify',
|
|
@@ -33,7 +64,7 @@ describe('selectStrategy', () => {
|
|
|
33
64
|
});
|
|
34
65
|
|
|
35
66
|
describe('verifyConfiguration', () => {
|
|
36
|
-
it('resolves the repo root
|
|
67
|
+
it('resolves the repo root and gets rid of trailing slashes', () => {
|
|
37
68
|
const result = verifyConfiguration([
|
|
38
69
|
{ folder: '.', actorFullName: 'myteam/root', tokenEnvVar: 'APIFY_TOKEN', overrideActorContext: undefined },
|
|
39
70
|
{
|
|
@@ -44,15 +75,11 @@ describe('verifyConfiguration', () => {
|
|
|
44
75
|
},
|
|
45
76
|
]);
|
|
46
77
|
|
|
47
|
-
expect(result).
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
tokenEnvVar: 'APIFY_TOKEN',
|
|
53
|
-
overrideActorContext: ['packages'],
|
|
54
|
-
},
|
|
55
|
-
]);
|
|
78
|
+
expect(result[0].folder).toBeInstanceOf(ExistingDir);
|
|
79
|
+
expect(result[0].folder.path).toBe('.');
|
|
80
|
+
expect(result[1].folder.path).toBe('actors/shopify');
|
|
81
|
+
expect(result[1].overrideActorContext?.[0]).toBeInstanceOf(RelativeDir);
|
|
82
|
+
expect(result[1].overrideActorContext?.[0].path).toBe('packages');
|
|
56
83
|
});
|
|
57
84
|
|
|
58
85
|
it.each([
|
|
@@ -123,26 +150,42 @@ describe('verifyConfiguration', () => {
|
|
|
123
150
|
});
|
|
124
151
|
|
|
125
152
|
describe('parseConfigFile', () => {
|
|
126
|
-
it('validates and normalizes
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
153
|
+
it('validates and normalizes paths after resolving the config', () => {
|
|
154
|
+
const [parsed] = parseConfigFile({ actors: [actor({ folder: './bin/', overrideActorContext: ['test/'] })] });
|
|
155
|
+
expect(parsed.folder).toBeInstanceOf(ExistingDir);
|
|
156
|
+
expect(parsed.folder.path).toBe('bin');
|
|
157
|
+
expect(parsed.overrideActorContext?.[0]).toBeInstanceOf(RelativeDir);
|
|
158
|
+
expect(parsed.overrideActorContext?.[0].path).toBe('test');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('accepts the repo root as an existing directory', () => {
|
|
162
|
+
expect(parseConfigFile({ actors: [actor({ folder: '' })] })[0].folder.path).toBe('.');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('reports invalid paths at their config fields', () => {
|
|
166
|
+
expect(() => parseConfigFile({ actors: [actor({ folder: 'missing-actor-dir' })] })).toThrow(/\[0\]\.folder/);
|
|
167
|
+
expect(() => parseConfigFile({ actors: [actor({ folder: 'package.json' })] })).toThrow(/to be a directory/);
|
|
168
|
+
expect(() =>
|
|
169
|
+
parseConfigFile({ actors: [actor({ folder: 'bin', overrideActorContext: ['../outside'] })] }),
|
|
170
|
+
).toThrow(/\[0\]\.overrideActorContext\[0\]/);
|
|
130
171
|
});
|
|
131
172
|
|
|
132
173
|
it('parses a grouped config file, which needs mode to be stripped', () => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
tokenEnvVar: 'APIFY_TOKEN',
|
|
140
|
-
},
|
|
174
|
+
const parsed = parseConfigFile({
|
|
175
|
+
mode: CONFIG_FILE_STRATEGY.GROUPED,
|
|
176
|
+
groups: {
|
|
177
|
+
myteam: {
|
|
178
|
+
actors: [{ folder: 'actors/shopify/', actorFullName: 'myteam/shopify' }],
|
|
179
|
+
tokenEnvVar: 'APIFY_TOKEN',
|
|
141
180
|
},
|
|
142
|
-
}
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
]);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
expect(parsed).toHaveLength(1);
|
|
184
|
+
expect(parsed[0].folder).toBeInstanceOf(ExistingDir);
|
|
185
|
+
expect(parsed[0].folder.path).toBe('actors/shopify');
|
|
186
|
+
expect(parsed[0].actorFullName).toBe('myteam/shopify');
|
|
187
|
+
expect(parsed[0].tokenEnvVar).toBe('APIFY_TOKEN');
|
|
188
|
+
expect(parsed[0].overrideActorContext).toBeUndefined();
|
|
146
189
|
});
|
|
147
190
|
|
|
148
191
|
it('surfaces cross-entry violations from verifyConfiguration', () => {
|
|
@@ -155,4 +198,15 @@ describe('parseConfigFile', () => {
|
|
|
155
198
|
}),
|
|
156
199
|
).toThrow(/Duplicate folder/);
|
|
157
200
|
});
|
|
201
|
+
|
|
202
|
+
it('rejects folders that collide after resolving dot segments', () => {
|
|
203
|
+
expect(() =>
|
|
204
|
+
parseConfigFile({
|
|
205
|
+
actors: [
|
|
206
|
+
actor({ actorFullName: 'myteam/actor-a', folder: 'bin/../test' }),
|
|
207
|
+
actor({ actorFullName: 'myteam/actor-b', folder: 'test' }),
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
).toThrow('Duplicate folder "test"');
|
|
211
|
+
});
|
|
158
212
|
});
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import { fileURLToPath } from 'node:url';
|
|
4
3
|
|
|
5
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
5
|
|
|
7
6
|
import { safeReadJsonObjectFile } from '../../../../bin/utils/json-file.js';
|
|
7
|
+
import { ExistingFile } from '../../../../bin/utils/path/repo-relative.js';
|
|
8
8
|
|
|
9
|
-
// These tests read real fixture files rather than mocking `node:fs
|
|
10
|
-
//
|
|
11
|
-
// symlink, unreadable file), so mocking those away would only assert the mock back to itself.
|
|
9
|
+
// These tests read real fixture files rather than mocking `node:fs`. Existence and file-kind checks
|
|
10
|
+
// happen when constructing ExistingFile; this reader tests JSON parsing and read failures after that.
|
|
12
11
|
const FIXTURE_DIR = fileURLToPath(new URL('../../../fixtures/bin/utils/files/', import.meta.url));
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
let originalCwd: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
originalCwd = process.cwd();
|
|
17
|
+
process.chdir(FIXTURE_DIR);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => process.chdir(originalCwd));
|
|
21
|
+
|
|
22
|
+
const fixture = (name: string): ExistingFile => new ExistingFile(name);
|
|
15
23
|
|
|
16
24
|
const NESTED_FIXTURE = {
|
|
17
25
|
storages: { datasets: { default: { title: 'Default' } } },
|
|
@@ -42,29 +50,6 @@ describe('safeReadJsonObjectFile', () => {
|
|
|
42
50
|
});
|
|
43
51
|
|
|
44
52
|
describe('filesystem-level handling', () => {
|
|
45
|
-
it('fails when the path does not exist', () => {
|
|
46
|
-
const filePath = fixture('does-not-exist.json');
|
|
47
|
-
|
|
48
|
-
const result = safeReadJsonObjectFile(filePath);
|
|
49
|
-
|
|
50
|
-
expect(result).toMatchObject({
|
|
51
|
-
success: false,
|
|
52
|
-
reason: 'unreadable',
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// The fixture is named `directory.json` on purpose — the check is `stat().isFile()`, not the extension.
|
|
57
|
-
it('fails when the path is a directory', () => {
|
|
58
|
-
const filePath = fixture('directory.json');
|
|
59
|
-
|
|
60
|
-
const result = safeReadJsonObjectFile(filePath);
|
|
61
|
-
|
|
62
|
-
expect(result).toMatchObject({
|
|
63
|
-
success: false,
|
|
64
|
-
reason: 'not-a-file',
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
53
|
// Regression guard: `lstat` would report the link itself as "not a file" and reject it, even
|
|
69
54
|
// though `readFile` follows symlinks and would have read the target without complaint.
|
|
70
55
|
it('follows a symlink to its target file', () => {
|
|
@@ -76,14 +61,15 @@ describe('safeReadJsonObjectFile', () => {
|
|
|
76
61
|
// Git only tracks the executable bit, so this is the one case whose fixture cannot carry its own
|
|
77
62
|
// mode — `unreadable.json` is committed readable and stripped of permissions just for this test.
|
|
78
63
|
describe('unreadable file', () => {
|
|
79
|
-
const filePath =
|
|
64
|
+
const filePath = 'unreadable.json';
|
|
80
65
|
|
|
81
66
|
afterEach(async () => fs.chmod(filePath, 0o644));
|
|
82
67
|
|
|
83
68
|
it.runIf(process.getuid?.() !== 0)('fails when the file exists but cannot be read', async () => {
|
|
69
|
+
const file = fixture(filePath);
|
|
84
70
|
await fs.chmod(filePath, 0o000);
|
|
85
71
|
|
|
86
|
-
const result = safeReadJsonObjectFile(
|
|
72
|
+
const result = safeReadJsonObjectFile(file);
|
|
87
73
|
|
|
88
74
|
expect(result).toMatchObject({
|
|
89
75
|
success: false,
|
|
@@ -135,7 +121,7 @@ describe('safeReadJsonObjectFile', () => {
|
|
|
135
121
|
const result = safeReadJsonObjectFile(filePath);
|
|
136
122
|
|
|
137
123
|
expect(result).toMatchObject({ success: false, reason: 'invalid-json' });
|
|
138
|
-
expect((result as { failure: Error }).failure.message).toContain(`Failed to parse ${filePath}`);
|
|
124
|
+
expect((result as { failure: Error }).failure.message).toContain(`Failed to parse ${filePath.path}`);
|
|
139
125
|
});
|
|
140
126
|
|
|
141
127
|
it('fails on an empty file', () => {
|
|
@@ -144,7 +130,7 @@ describe('safeReadJsonObjectFile', () => {
|
|
|
144
130
|
const result = safeReadJsonObjectFile(filePath);
|
|
145
131
|
|
|
146
132
|
expect(result).toMatchObject({ success: false, reason: 'invalid-json' });
|
|
147
|
-
expect((result as { failure: Error }).failure.message).toContain(`Failed to parse ${filePath}`);
|
|
133
|
+
expect((result as { failure: Error }).failure.message).toContain(`Failed to parse ${filePath.path}`);
|
|
148
134
|
});
|
|
149
135
|
|
|
150
136
|
it.fails('reads jsonc', () => {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ExistingDir,
|
|
7
|
+
ExistingFile,
|
|
8
|
+
RelativeDir,
|
|
9
|
+
type RelativeFile,
|
|
10
|
+
} from '../../../../../bin/utils/path/repo-relative.js';
|
|
11
|
+
|
|
12
|
+
// Paths from the config file and from git all enter through the root.
|
|
13
|
+
const { ROOT } = RelativeDir;
|
|
14
|
+
const dir = (raw: string): RelativeDir => ROOT.joinDir(raw);
|
|
15
|
+
const file = (raw: string): RelativeFile => ROOT.joinFile(raw);
|
|
16
|
+
|
|
17
|
+
describe('ExistingDir', () => {
|
|
18
|
+
it('validates directories through both construction paths', () => {
|
|
19
|
+
expect(ExistingDir.initialize(dir('bin')).path).toBe('bin');
|
|
20
|
+
expect(new ExistingDir('./bin/').path).toBe('bin');
|
|
21
|
+
expect(new ExistingDir('.').path).toBe('.');
|
|
22
|
+
expect(() => ExistingDir.initialize(dir('missing-dir'))).toThrow('to exist');
|
|
23
|
+
expect(() => new ExistingDir('package.json')).toThrow('to be a directory');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('preserves directory equality after existence validation', () => {
|
|
27
|
+
const relative = dir('./bin');
|
|
28
|
+
const existing = ExistingDir.initialize(relative);
|
|
29
|
+
|
|
30
|
+
expect(relative.isEqualTo(existing)).toBe(true);
|
|
31
|
+
expect(existing.isEqualTo(relative)).toBe(true);
|
|
32
|
+
expect(existing.isEqualTo(file('bin'))).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('ExistingFile', () => {
|
|
37
|
+
it('validates files through initialize', () => {
|
|
38
|
+
expect(ExistingFile.initialize(file('package.json')).path).toBe('package.json');
|
|
39
|
+
expect(() => ExistingFile.initialize(file('package.json/missing'))).toThrow();
|
|
40
|
+
expect(() => ExistingFile.initialize(file('bin'))).toThrow('to be a file');
|
|
41
|
+
expect(() => new ExistingFile('missing.txt')).toThrow('to exist');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('preserves file equality after existence validation', () => {
|
|
45
|
+
const relative = file('./package.json');
|
|
46
|
+
const existing = ExistingFile.initialize(relative);
|
|
47
|
+
|
|
48
|
+
expect(relative.isEqualTo(existing)).toBe(true);
|
|
49
|
+
expect(existing.isEqualTo(relative)).toBe(true);
|
|
50
|
+
expect(existing.isEqualTo(ExistingFile.initialize(file('package.json')))).toBe(true);
|
|
51
|
+
expect(existing.isEqualTo(file('other.json'))).toBe(false);
|
|
52
|
+
expect(existing.isEqualTo(dir('package.json'))).toBe(false);
|
|
53
|
+
expect(dir('package.json').isEqualTo(existing)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('RelativeDir', () => {
|
|
58
|
+
describe('ROOT', () => {
|
|
59
|
+
it('is a RelativeDir spelled "."', () => {
|
|
60
|
+
expect(ROOT).toBeInstanceOf(RelativeDir);
|
|
61
|
+
expect(ROOT.toString()).toBe('.');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each(['', '.', './', './/', 'a/..'])('is what %j parses to', (raw) => {
|
|
65
|
+
expect(dir(raw).isEqualTo(ROOT)).toBe(true);
|
|
66
|
+
expect(dir(raw).toString()).toBe('.');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('parsing (ROOT.joinDir)', () => {
|
|
71
|
+
it.each(['actors/foo', './actors/foo', 'actors/foo/', 'actors//foo', './actors/./foo//', 'actors/bar/../foo'])(
|
|
72
|
+
'normalizes %j to actors/foo',
|
|
73
|
+
(raw) => {
|
|
74
|
+
expect(dir(raw).toString()).toBe('actors/foo');
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
it.each(['..', '../x', 'actors/../../x', './..'])('rejects %j because it escapes the repo root', (raw) => {
|
|
79
|
+
expect(() => dir(raw)).toThrow('escapes the repo root');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects absolute paths', () => {
|
|
83
|
+
expect(() => file('/etc/passwd')).toThrow('got absolute path "/etc/passwd"');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('rejects absolute paths even when they point inside the working directory', () => {
|
|
87
|
+
expect(() => dir(path.join(process.cwd(), 'actors/foo'))).toThrow('got absolute path');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('reports the path as written, not its normalized form', () => {
|
|
91
|
+
expect(() => dir('a/../../x')).toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('keeps a directory named "..foo", which does not escape the repo root', () => {
|
|
95
|
+
expect(dir('..foo/bar').toString()).toBe('..foo/bar');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('keeps case, since git paths are case-sensitive', () => {
|
|
99
|
+
expect(dir('Actors/Foo').isEqualTo(dir('actors/foo'))).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('joinDir / joinFile', () => {
|
|
104
|
+
const actorDir = dir('actors/foo');
|
|
105
|
+
const actorDotDir = dir('actors/foo/.actor');
|
|
106
|
+
|
|
107
|
+
it('resolves relative to the path it is called on', () => {
|
|
108
|
+
expect(actorDir.joinDir('src').toString()).toBe('actors/foo/src');
|
|
109
|
+
expect(actorDir.joinFile('src/main.ts').toString()).toBe('actors/foo/src/main.ts');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('resolves a dockerContextDir the way actor.json means it', () => {
|
|
113
|
+
expect(actorDotDir.joinDir('..').toString()).toBe('actors/foo');
|
|
114
|
+
expect(actorDotDir.joinDir('../..').toString()).toBe('actors');
|
|
115
|
+
expect(actorDotDir.joinDir('../../..').isEqualTo(ROOT)).toBe(true);
|
|
116
|
+
expect(actorDotDir.joinFile('./Dockerfile').toString()).toBe('actors/foo/.actor/Dockerfile');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('can climb out of the path it is called on, as long as it stays within the repo root', () => {
|
|
120
|
+
expect(actorDir.joinDir('../bar').toString()).toBe('actors/bar');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it.each(['', '.', '..', 'src/', 'src/.', 'src/..'])('rejects %j as a file path', (raw) => {
|
|
124
|
+
expect(() => actorDir.joinFile(raw)).toThrow('Expected a file path');
|
|
125
|
+
expect(() => file(`actors/foo/${raw}`)).toThrow('Expected a file path');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('rejects a file path through a file, too', () => {
|
|
129
|
+
expect(() => file('actors/foo/main.ts').joinFile('.')).toThrow('Expected a file path');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('rejects results that escape the repo root', () => {
|
|
133
|
+
expect(() => actorDotDir.joinDir('../../../..')).toThrow('escapes the repo root');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not depend on the process working directory', () => {
|
|
137
|
+
const originalCwd = process.cwd();
|
|
138
|
+
try {
|
|
139
|
+
process.chdir(path.join(originalCwd, 'bin'));
|
|
140
|
+
expect(dir('actors/foo').toString()).toBe('actors/foo');
|
|
141
|
+
expect(actorDir.joinDir('src').toString()).toBe('actors/foo/src');
|
|
142
|
+
} finally {
|
|
143
|
+
process.chdir(originalCwd);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('isEqualTo', () => {
|
|
149
|
+
it('matches different spellings of the same path', () => {
|
|
150
|
+
expect(dir('./actors/foo/').isEqualTo(dir('actors/foo'))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not match different paths', () => {
|
|
154
|
+
expect(dir('actors/foo').isEqualTo(dir('actors/foobar'))).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not match a file and a directory spelled the same', () => {
|
|
158
|
+
expect(dir('actors/foo').isEqualTo(file('actors/foo'))).toBe(false);
|
|
159
|
+
expect(file('actors/foo').isEqualTo(dir('actors/foo'))).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('isStrictAncestorOf', () => {
|
|
164
|
+
const foo = dir('actors/foo');
|
|
165
|
+
|
|
166
|
+
it('is true for paths inside it', () => {
|
|
167
|
+
expect(foo.isStrictAncestorOf(file('actors/foo/src/main.ts'))).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('is false for itself', () => {
|
|
171
|
+
expect(foo.isStrictAncestorOf(foo)).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('is false for a sibling that shares a name prefix', () => {
|
|
175
|
+
expect(foo.isStrictAncestorOf(dir('actors/foobar'))).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('makes the root an ancestor of every other path', () => {
|
|
179
|
+
expect(ROOT.isStrictAncestorOf(foo)).toBe(true);
|
|
180
|
+
expect(ROOT.isStrictAncestorOf(file('README.md'))).toBe(true);
|
|
181
|
+
expect(ROOT.isStrictAncestorOf(ROOT)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('contains', () => {
|
|
186
|
+
const foo = dir('actors/foo');
|
|
187
|
+
|
|
188
|
+
it('matches itself and paths inside it', () => {
|
|
189
|
+
expect(foo.contains(foo)).toBe(true);
|
|
190
|
+
expect(foo.contains(file('actors/foo/src/main.ts'))).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('does not match a sibling that shares a name prefix', () => {
|
|
194
|
+
expect(foo.contains(file('actors/foobar/main.ts'))).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('makes the root contain every path', () => {
|
|
198
|
+
expect(ROOT.contains(foo)).toBe(true);
|
|
199
|
+
expect(ROOT.contains(file('README.md'))).toBe(true);
|
|
200
|
+
expect(ROOT.contains(ROOT)).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('does not contain a file spelled the same as itself', () => {
|
|
204
|
+
expect(foo.contains(file('actors/foo'))).toBe(false);
|
|
205
|
+
expect(foo.isStrictAncestorOf(file('actors/foo'))).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('does not make a subfolder contain the root', () => {
|
|
209
|
+
expect(foo.contains(ROOT)).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('string output', () => {
|
|
214
|
+
it('renders as the path in templates', () => {
|
|
215
|
+
expect(`${dir('actors/foo')}/x`).toBe('actors/foo/x');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('renders as a plain string in JSON', () => {
|
|
219
|
+
expect(JSON.stringify({ folder: dir('actors/foo'), root: ROOT })).toBe(
|
|
220
|
+
'{"folder":"actors/foo","root":"."}',
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Git and the config file always use "/", so paths must not change spelling with the host OS.
|
|
226
|
+
describe('on Windows', () => {
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
vi.doUnmock('node:path');
|
|
229
|
+
vi.resetModules();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const importWithWin32Path = async (): Promise<typeof RelativeDir> => {
|
|
233
|
+
vi.resetModules();
|
|
234
|
+
vi.doMock('node:path', () => ({ default: path.win32, ...path.win32 }));
|
|
235
|
+
const module = await import('../../../../../bin/utils/path/repo-relative.js');
|
|
236
|
+
return module.RelativeDir;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
it('keeps forward slashes', async () => {
|
|
240
|
+
const WinRelativeDir = await importWithWin32Path();
|
|
241
|
+
expect(WinRelativeDir.ROOT.joinDir('actors/foo').toString()).toBe('actors/foo');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('matches paths inside a scope', async () => {
|
|
245
|
+
const WinRelativeDir = await importWithWin32Path();
|
|
246
|
+
const foo = WinRelativeDir.ROOT.joinDir('actors/foo');
|
|
247
|
+
expect(foo.contains(WinRelativeDir.ROOT.joinFile('actors/foo/src/main.ts'))).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|