@fishawack/lab-env 5.4.0 → 5.5.0-beta.1
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/CHANGELOG.md +20 -0
- package/_Ai/python-3.md +31 -28
- package/_Ai/workspace-@.md +4 -0
- package/_Test/_fixtures/content/config.json +36 -0
- package/_Test/_fixtures/content/content-0/blah.txt +1 -0
- package/_Test/content.js +517 -0
- package/cli.js +1 -0
- package/commands/content.js +115 -1
- package/commands/create/cmds/provision.js +1 -1
- package/commands/create/libs/aws-cloudfront-request.js +1 -1
- package/commands/create/libs/aws-cloudfront-response.js +1 -1
- package/commands/create/libs/vars.js +4 -0
- package/commands/create/templates/elasticbeanstalk/.platform/httpd/conf.d/security_headers-wordpress.conf +1 -1
- package/commands/create/templates/elasticbeanstalk/.platform/httpd/conf.d/security_headers.conf +1 -1
- package/commands/create/templates/elasticbeanstalk/.platform/nginx/conf.d/security_headers.conf +1 -1
- package/commands/helpers/content-pull.js +209 -0
- package/commands/helpers/content-request.js +223 -0
- package/commands/scan.js +49 -0
- package/commands/test.js +2 -15
- package/commands/workspace.js +160 -63
- package/package.json +3 -1
- package/python/0/docker-compose.yml +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
+
### 5.5.0-beta.1 (2026-04-13)
|
|
4
|
+
|
|
5
|
+
#### Features
|
|
6
|
+
|
|
7
|
+
* content logic from now moved to lab-env in backwards compatible way ([2d836ce](https://bitbucket.org/fishawackdigital/lab-env/commits/2d836cec5bc9d429b7fdc544c5fc1e8c3b749124))
|
|
8
|
+
* extract snyk scan into standalone fw scan command ([abba628](https://bitbucket.org/fishawackdigital/lab-env/commits/abba628183fa94da5a21a14eb014c13a23036f84))
|
|
9
|
+
* **python:** added healthcheck ([ca6e323](https://bitbucket.org/fishawackdigital/lab-env/commits/ca6e323487aaae201eabe34e5208845585d182d4))
|
|
10
|
+
* **workspace:** support adding and removing projects from existing workspaces ([d70bf41](https://bitbucket.org/fishawackdigital/lab-env/commits/d70bf4130dffe412a15e8d8c6c3e6777daac65d3))
|
|
11
|
+
|
|
12
|
+
#### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* allow blob as default-src on csp ([3cea3d4](https://bitbucket.org/fishawackdigital/lab-env/commits/3cea3d4bbd8bcd0ffdb6293967e4248ed542e8cf))
|
|
15
|
+
* csps now include worker-src with blob and self support ([bbe35b8](https://bitbucket.org/fishawackdigital/lab-env/commits/bbe35b807153d1af53bad0872cc6461b8c39e92f))
|
|
16
|
+
* scans now all run before errors thrown and always tackle lock files ([f212df0](https://bitbucket.org/fishawackdigital/lab-env/commits/f212df0cc1e6d5373c6bffcbde7e4e1ac2784db6))
|
|
17
|
+
* split snyk scans per platform with project-name flag ([7d89539](https://bitbucket.org/fishawackdigital/lab-env/commits/7d895395359e6f9d594e264f9b0bcf2e5a59c3bd))
|
|
18
|
+
|
|
19
|
+
#### Build Updates
|
|
20
|
+
|
|
21
|
+
* added python as boilerplate option ([8695c48](https://bitbucket.org/fishawackdigital/lab-env/commits/8695c48a763f623fe852b029663d8f2d958ba383))
|
|
22
|
+
|
|
3
23
|
### 5.4.0 (2026-03-12)
|
|
4
24
|
|
|
5
25
|
#### Features
|
package/_Ai/python-3.md
CHANGED
|
@@ -1,42 +1,45 @@
|
|
|
1
1
|
---
|
|
2
|
-
applyTo: "
|
|
2
|
+
applyTo: "**/*.py"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Python Project Instructions
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Package Management
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Uses **uv** (not pip). Dependencies defined in `pyproject.toml` with `uv.lock`. No `requirements.txt`.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
All commands run through the `fw` container orchestration manager:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- The project uses FastAPI and Uvicorn for the web server. Upon running the project, it will be accessible at `http://localhost:3000` and it'll keep running as long as the process is active. For easier development, pipe the output to a log file.
|
|
20
|
-
- The project must always try to stay stateless and handle requests with in-memory processing whenever possible.
|
|
21
|
-
- The project must always fail gracefully and return appropriate error messages.
|
|
13
|
+
```bash
|
|
14
|
+
fw uv add <dep> # Add a dependency
|
|
15
|
+
fw uv add --dev <dep> # Add a dev dependency
|
|
16
|
+
fw uv sync # Install/sync all dependencies
|
|
17
|
+
fw uv run <command> # Run a command in the project environment
|
|
18
|
+
```
|
|
22
19
|
|
|
23
|
-
##
|
|
20
|
+
## Stack
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
- Python >=3.13 with FastAPI + Uvicorn
|
|
23
|
+
- `.env` loaded via `python-dotenv`
|
|
24
|
+
- Stateless request handling; fail gracefully with appropriate error responses
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
# Update dependencies
|
|
29
|
-
pip install -r ./src/requirements.txt
|
|
26
|
+
## Project Structure
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
- Entry point: `src/main.py`
|
|
29
|
+
- Routes: `src/routes/`
|
|
30
|
+
- Tests: `tests/` (run with `fw uv run pytest`)
|
|
31
|
+
- Dev dependencies in `[dependency-groups.dev]` of `pyproject.toml`
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
## Tests
|
|
34
|
+
|
|
35
|
+
You can run tests via
|
|
36
|
+
|
|
37
|
+
`fw test`
|
|
38
|
+
|
|
39
|
+
or an individual test file with
|
|
40
|
+
|
|
41
|
+
`fw uv run pytest -o log_cli=true tests/integration/test_middleware.py`
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
or a single test within that file with
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
- **Routes**: API routes are defined in `./src/routes/*.py`
|
|
42
|
-
- **Connectors**: LLM Connectors are defined in `./src/connectors/*.py`
|
|
45
|
+
`fw uv run pytest -o log_cli=true tests/integration/test_middleware.py -k 'test_001'`
|
package/_Ai/workspace-@.md
CHANGED
|
@@ -9,3 +9,7 @@ Backends are typically named with the pattern \*-api.
|
|
|
9
9
|
Frontends are typically named with the pattern \*-fe. or with no suffix.
|
|
10
10
|
|
|
11
11
|
Ensure you cd into the correct directory before running any terminal commands.
|
|
12
|
+
|
|
13
|
+
## Commits
|
|
14
|
+
|
|
15
|
+
When a feature is complete always suggest the conventional commit that summarizes the changes.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"attributes": {
|
|
3
|
+
"src": "_Build",
|
|
4
|
+
"targets": {
|
|
5
|
+
"aws-s3": {
|
|
6
|
+
"content": [{ "aws-s3": "fishawack", "location": "core-test-suite-content" }]
|
|
7
|
+
},
|
|
8
|
+
"aws-s3-nested": {
|
|
9
|
+
"content": [{ "aws-s3": "fishawack", "location": "core-test-suite-content/nested" }]
|
|
10
|
+
},
|
|
11
|
+
"aws-s3-key": {
|
|
12
|
+
"content": [{ "aws-s3": "fishawack", "location": "core-test-suite-content", "key": "custom-key" }]
|
|
13
|
+
},
|
|
14
|
+
"aws-s3-sync": {
|
|
15
|
+
"content": [{ "aws-s3": "fishawack", "location": "core-test-suite-content", "key": "custom-key", "sync": true }]
|
|
16
|
+
},
|
|
17
|
+
"contentful": {
|
|
18
|
+
"content": [{ "url": "https://cdn.contentful.com/", "api": "spaces/hnifelsg7djt/environments/master/entries?access_token=-_qKPGeSi_ksTsXJd9o9KZdl-jdxZTDd_E9gjLS4QHY&content_type=", "type": "contentful", "bundle": true, "find": "\/\/images.ctfassets.net\/(.*?)\/(.*?)\/(.*?)\/", "endpoints": ["layout", "layoutCopy", "lessonImage"] }]
|
|
19
|
+
},
|
|
20
|
+
"empty": { "content": [] },
|
|
21
|
+
"handover": {},
|
|
22
|
+
"ftp-legacy": {
|
|
23
|
+
"content": [{ "ftp": "ftp-fishawack.egnyte.com", "location": "Shared/FW/Knutsford/Digital/Auto-Content/test-shared/" }]
|
|
24
|
+
},
|
|
25
|
+
"single": {
|
|
26
|
+
"content": [{ "aws-s3": "fishawack", "location": "core-test-suite-content" }]
|
|
27
|
+
},
|
|
28
|
+
"double": {
|
|
29
|
+
"content": [
|
|
30
|
+
{ "aws-s3": "fishawack", "location": "core-test-suite-content" },
|
|
31
|
+
{ "aws-s3": "fishawack", "location": "core-test-suite-content", "key": "custom-key" }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blah
|
package/_Test/content.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const expect = require("chai").expect;
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs-extra");
|
|
6
|
+
const glob = require("glob");
|
|
7
|
+
const { isEqual } = require("lodash");
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
pullS3,
|
|
11
|
+
parseBucketAndPrefix,
|
|
12
|
+
getLocalFiles,
|
|
13
|
+
createClient,
|
|
14
|
+
} = require("../commands/helpers/content-pull.js");
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
pullRequests,
|
|
18
|
+
download,
|
|
19
|
+
rewrite,
|
|
20
|
+
urlJoin,
|
|
21
|
+
} = require("../commands/helpers/content-request.js");
|
|
22
|
+
|
|
23
|
+
const fixtureDir = path.join(__dirname, "_fixtures", "content");
|
|
24
|
+
const outputDir = path.join(fixtureDir, "_Build", "content");
|
|
25
|
+
const snapshotPath = path.join(fixtureDir, ".tmp", "content.json");
|
|
26
|
+
|
|
27
|
+
function loadFixtureConfig(branch) {
|
|
28
|
+
const config = fs.readJSONSync(path.join(fixtureDir, "config.json"));
|
|
29
|
+
const branchConfig = config.attributes.targets[branch] || {};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...config.attributes,
|
|
33
|
+
...branchConfig,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("content", () => {
|
|
38
|
+
describe("parseBucketAndPrefix", () => {
|
|
39
|
+
it("Should parse bucket only when no slash", () => {
|
|
40
|
+
const result = parseBucketAndPrefix("my-bucket");
|
|
41
|
+
expect(result.Bucket).to.equal("my-bucket");
|
|
42
|
+
expect(result.Prefix).to.equal("");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("Should parse bucket and prefix when slash present", () => {
|
|
46
|
+
const result = parseBucketAndPrefix("my-bucket/some/prefix");
|
|
47
|
+
expect(result.Bucket).to.equal("my-bucket");
|
|
48
|
+
expect(result.Prefix).to.equal("some/prefix");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Should handle single level prefix", () => {
|
|
52
|
+
const result = parseBucketAndPrefix("my-bucket/folder");
|
|
53
|
+
expect(result.Bucket).to.equal("my-bucket");
|
|
54
|
+
expect(result.Prefix).to.equal("folder");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("getLocalFiles", () => {
|
|
59
|
+
it("Should return empty array for non-existent directory", () => {
|
|
60
|
+
const files = getLocalFiles(path.join(fixtureDir, "non-existent"));
|
|
61
|
+
expect(files).to.be.an("array").that.is.empty;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("Should return file list with relative paths", () => {
|
|
65
|
+
const files = getLocalFiles(path.join(fixtureDir, "content-0"));
|
|
66
|
+
expect(files).to.be.an("array").that.is.not.empty;
|
|
67
|
+
expect(files[0]).to.have.property("relativePath");
|
|
68
|
+
expect(files[0]).to.have.property("mtime");
|
|
69
|
+
expect(files[0]).to.have.property("size");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("urlJoin", () => {
|
|
74
|
+
it("Should join base URL with path segments", () => {
|
|
75
|
+
const result = urlJoin(
|
|
76
|
+
"https://example.com",
|
|
77
|
+
"/api/v1/",
|
|
78
|
+
"endpoint",
|
|
79
|
+
);
|
|
80
|
+
expect(result).to.contain("example.com");
|
|
81
|
+
expect(result).to.contain("endpoint");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("Should handle trailing slashes", () => {
|
|
85
|
+
const result = urlJoin(
|
|
86
|
+
"https://example.com/",
|
|
87
|
+
"api/v1/",
|
|
88
|
+
"endpoint",
|
|
89
|
+
);
|
|
90
|
+
expect(result).to.include("api/v1/endpoint");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("content:pull — S3", () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
fs.removeSync(outputDir);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("Should pull down assets from s3 via aws sdk", async () => {
|
|
100
|
+
const config = loadFixtureConfig("aws-s3");
|
|
101
|
+
|
|
102
|
+
await pullS3(config.content[0], path.join(outputDir, "content-0"));
|
|
103
|
+
|
|
104
|
+
expect(glob.sync(path.join(outputDir, "content-0/**/*"))).to.be.an(
|
|
105
|
+
"array",
|
|
106
|
+
).that.is.not.empty;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("Should pull down assets from s3 when nested", async () => {
|
|
110
|
+
const config = loadFixtureConfig("aws-s3-nested");
|
|
111
|
+
|
|
112
|
+
await pullS3(config.content[0], path.join(outputDir, "content-0"));
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
fs.readFileSync(path.join(outputDir, "content-0/test.txt"), {
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
}),
|
|
118
|
+
).to.equal("core-test-suite-content-nested");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("Should store in directory with key name when defined", async () => {
|
|
122
|
+
const config = loadFixtureConfig("aws-s3-key");
|
|
123
|
+
|
|
124
|
+
await pullS3(config.content[0], path.join(outputDir, "custom-key"));
|
|
125
|
+
|
|
126
|
+
expect(glob.sync(path.join(outputDir, "custom-key/**/*"))).to.be.an(
|
|
127
|
+
"array",
|
|
128
|
+
).that.is.not.empty;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("Should sync new assets from s3 (bidirectional)", async () => {
|
|
132
|
+
const config = loadFixtureConfig("aws-s3-sync");
|
|
133
|
+
const saveTo = path.join(outputDir, "custom-key");
|
|
134
|
+
const profile = config.content[0]["aws-s3"];
|
|
135
|
+
const client = createClient(profile);
|
|
136
|
+
const { Bucket, Prefix } = parseBucketAndPrefix(
|
|
137
|
+
config.content[0].location,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Clean up any previous sync test file
|
|
141
|
+
try {
|
|
142
|
+
const { DeleteObjectCommand } = require("@aws-sdk/client-s3");
|
|
143
|
+
await client.send(
|
|
144
|
+
new DeleteObjectCommand({
|
|
145
|
+
Bucket,
|
|
146
|
+
Key: Prefix
|
|
147
|
+
? `${Prefix}/sync-test.txt`
|
|
148
|
+
: "sync-test.txt",
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
} catch {
|
|
152
|
+
/* may not exist */
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// First pull to get existing content
|
|
156
|
+
await pullS3(config.content[0], saveTo);
|
|
157
|
+
|
|
158
|
+
// Create a local file to sync up
|
|
159
|
+
fs.mkdirpSync(saveTo);
|
|
160
|
+
fs.writeFileSync(path.join(saveTo, "sync-test.txt"), "sync-test");
|
|
161
|
+
|
|
162
|
+
// Second pull should upload local file then download remote
|
|
163
|
+
await pullS3(config.content[0], saveTo);
|
|
164
|
+
|
|
165
|
+
// Clean local and re-pull — the synced file should come back
|
|
166
|
+
fs.removeSync(saveTo);
|
|
167
|
+
await pullS3(config.content[0], saveTo);
|
|
168
|
+
|
|
169
|
+
expect(() =>
|
|
170
|
+
fs.readFileSync(path.join(saveTo, "sync-test.txt"), {
|
|
171
|
+
encoding: "utf8",
|
|
172
|
+
}),
|
|
173
|
+
).to.not.throw();
|
|
174
|
+
|
|
175
|
+
// Cleanup remote sync test file
|
|
176
|
+
try {
|
|
177
|
+
const { DeleteObjectCommand } = require("@aws-sdk/client-s3");
|
|
178
|
+
await client.send(
|
|
179
|
+
new DeleteObjectCommand({
|
|
180
|
+
Bucket,
|
|
181
|
+
Key: Prefix
|
|
182
|
+
? `${Prefix}/sync-test.txt`
|
|
183
|
+
: "sync-test.txt",
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
} catch {
|
|
187
|
+
/* cleanup best-effort */
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("Should not re-download files when local is newer (--update semantics)", async () => {
|
|
192
|
+
const config = loadFixtureConfig("aws-s3");
|
|
193
|
+
const saveTo = path.join(outputDir, "content-0");
|
|
194
|
+
|
|
195
|
+
// First pull
|
|
196
|
+
await pullS3(config.content[0], saveTo);
|
|
197
|
+
|
|
198
|
+
const files = glob.sync(path.join(saveTo, "**/*"), {
|
|
199
|
+
nodir: true,
|
|
200
|
+
});
|
|
201
|
+
expect(files.length).to.be.greaterThan(0);
|
|
202
|
+
|
|
203
|
+
// Touch all local files to make them "newer"
|
|
204
|
+
const futureTime = new Date(Date.now() + 86400000);
|
|
205
|
+
files.forEach((f) => fs.utimesSync(f, futureTime, futureTime));
|
|
206
|
+
|
|
207
|
+
// Record mtime before second pull
|
|
208
|
+
const mtimesBefore = {};
|
|
209
|
+
files.forEach((f) => {
|
|
210
|
+
mtimesBefore[f] = fs.statSync(f).mtimeMs;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Second pull should skip all files
|
|
214
|
+
await pullS3(config.content[0], saveTo);
|
|
215
|
+
|
|
216
|
+
// Verify files were not overwritten
|
|
217
|
+
files.forEach((f) => {
|
|
218
|
+
expect(fs.statSync(f).mtimeMs).to.equal(mtimesBefore[f]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("Should skip non-S3 protocols with warning", async () => {
|
|
223
|
+
const config = loadFixtureConfig("ftp-legacy");
|
|
224
|
+
|
|
225
|
+
// pullS3 won't be called for non-s3 — this is handled in content.js
|
|
226
|
+
// Verify the config doesn't have aws-s3 key
|
|
227
|
+
expect(config.content[0]["aws-s3"]).to.be.undefined;
|
|
228
|
+
expect(config.content[0].ftp).to.equal("ftp-fishawack.egnyte.com");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
after(() => {
|
|
232
|
+
fs.removeSync(outputDir);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("config change detection", () => {
|
|
237
|
+
beforeEach(() => {
|
|
238
|
+
fs.removeSync(outputDir);
|
|
239
|
+
fs.removeSync(path.join(fixtureDir, ".tmp"));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("Should clean content folders when content property is an empty array", () => {
|
|
243
|
+
// Pre-populate content directory
|
|
244
|
+
fs.copySync(
|
|
245
|
+
path.join(fixtureDir, "content-0"),
|
|
246
|
+
path.join(outputDir, "content-0"),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const config = loadFixtureConfig("empty");
|
|
250
|
+
|
|
251
|
+
expect(config.content).to.be.an("array").that.is.empty;
|
|
252
|
+
expect(glob.sync(path.join(outputDir, "**/*"))).to.be.an("array")
|
|
253
|
+
.that.is.not.empty;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("Should detect when content config has not changed", () => {
|
|
257
|
+
const config = loadFixtureConfig("aws-s3");
|
|
258
|
+
|
|
259
|
+
// Save a snapshot
|
|
260
|
+
fs.mkdirpSync(path.dirname(snapshotPath));
|
|
261
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(config.content));
|
|
262
|
+
|
|
263
|
+
// Compare — should be equal
|
|
264
|
+
const prev = JSON.parse(
|
|
265
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
266
|
+
);
|
|
267
|
+
expect(isEqual(prev, config.content)).to.be.true;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("Should detect when content config has changed", () => {
|
|
271
|
+
const configOld = loadFixtureConfig("single");
|
|
272
|
+
const configNew = loadFixtureConfig("double");
|
|
273
|
+
|
|
274
|
+
// Save old snapshot
|
|
275
|
+
fs.mkdirpSync(path.dirname(snapshotPath));
|
|
276
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(configOld.content));
|
|
277
|
+
|
|
278
|
+
const prev = JSON.parse(
|
|
279
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
280
|
+
);
|
|
281
|
+
expect(isEqual(prev, configNew.content)).to.be.false;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("Should handle missing previous snapshot gracefully", () => {
|
|
285
|
+
// No snapshot file exists
|
|
286
|
+
expect(fs.existsSync(snapshotPath)).to.be.false;
|
|
287
|
+
|
|
288
|
+
let prev = null;
|
|
289
|
+
try {
|
|
290
|
+
prev = JSON.parse(
|
|
291
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
292
|
+
);
|
|
293
|
+
} catch {
|
|
294
|
+
// expected
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
expect(prev).to.be.null;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("Should clean content when config changes between pulls", async () => {
|
|
301
|
+
const configDouble = loadFixtureConfig("double");
|
|
302
|
+
|
|
303
|
+
// Simulate first pull creating two content dirs
|
|
304
|
+
fs.mkdirpSync(path.join(outputDir, "content-0"));
|
|
305
|
+
fs.mkdirpSync(path.join(outputDir, "custom-key"));
|
|
306
|
+
fs.writeFileSync(
|
|
307
|
+
path.join(outputDir, "content-0/file.txt"),
|
|
308
|
+
"test",
|
|
309
|
+
);
|
|
310
|
+
fs.writeFileSync(
|
|
311
|
+
path.join(outputDir, "custom-key/file.txt"),
|
|
312
|
+
"test",
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Save snapshot for "double" config
|
|
316
|
+
fs.mkdirpSync(path.dirname(snapshotPath));
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
snapshotPath,
|
|
319
|
+
JSON.stringify(configDouble.content),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(glob.sync(path.join(outputDir, "*")).length).to.equal(2);
|
|
323
|
+
|
|
324
|
+
// Now switch to "single" config — should detect the change
|
|
325
|
+
const configSingle = loadFixtureConfig("single");
|
|
326
|
+
const prev = JSON.parse(
|
|
327
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
328
|
+
);
|
|
329
|
+
expect(isEqual(prev, configSingle.content)).to.be.false;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("Should not clean content when config unchanged between pulls", () => {
|
|
333
|
+
const config = loadFixtureConfig("double");
|
|
334
|
+
|
|
335
|
+
// Simulate content dirs
|
|
336
|
+
fs.mkdirpSync(path.join(outputDir, "content-0"));
|
|
337
|
+
fs.mkdirpSync(path.join(outputDir, "custom-key"));
|
|
338
|
+
fs.mkdirpSync(path.join(outputDir, "fake-content"));
|
|
339
|
+
|
|
340
|
+
// Save snapshot
|
|
341
|
+
fs.mkdirpSync(path.dirname(snapshotPath));
|
|
342
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(config.content));
|
|
343
|
+
|
|
344
|
+
// Reload and compare — should be unchanged
|
|
345
|
+
const prev = JSON.parse(
|
|
346
|
+
fs.readFileSync(snapshotPath, { encoding: "utf8" }),
|
|
347
|
+
);
|
|
348
|
+
expect(isEqual(prev, config.content)).to.be.true;
|
|
349
|
+
|
|
350
|
+
// Fake-content should survive (not cleaned)
|
|
351
|
+
expect(glob.sync(path.join(outputDir, "*")).length).to.equal(3);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("Should skip cleaning when content property is undefined", () => {
|
|
355
|
+
// Pre-populate content
|
|
356
|
+
fs.copySync(
|
|
357
|
+
path.join(fixtureDir, "content-0"),
|
|
358
|
+
path.join(outputDir, "content-0"),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const config = loadFixtureConfig("handover");
|
|
362
|
+
|
|
363
|
+
// "handover" target has no content property
|
|
364
|
+
expect(config.content).to.be.undefined;
|
|
365
|
+
// Content should remain untouched
|
|
366
|
+
expect(glob.sync(path.join(outputDir, "**/*"))).to.be.an("array")
|
|
367
|
+
.that.is.not.empty;
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
after(() => {
|
|
371
|
+
fs.removeSync(outputDir);
|
|
372
|
+
fs.removeSync(path.join(fixtureDir, ".tmp"));
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe("content:request — HTTP APIs", () => {
|
|
377
|
+
describe("contentful", () => {
|
|
378
|
+
const saveTo = path.join(outputDir, "content-0");
|
|
379
|
+
|
|
380
|
+
before(async () => {
|
|
381
|
+
fs.removeSync(outputDir);
|
|
382
|
+
|
|
383
|
+
const config = loadFixtureConfig("contentful");
|
|
384
|
+
|
|
385
|
+
await pullRequests(config.content, fixtureDir + "/_Build");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("Should save to json files", () => {
|
|
389
|
+
expect(
|
|
390
|
+
fs
|
|
391
|
+
.lstatSync(path.join(saveTo, "media/layout.json"))
|
|
392
|
+
.isFile(),
|
|
393
|
+
).to.be.true;
|
|
394
|
+
expect(
|
|
395
|
+
fs
|
|
396
|
+
.lstatSync(path.join(saveTo, "media/layoutCopy.json"))
|
|
397
|
+
.isFile(),
|
|
398
|
+
).to.be.true;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("Should download images", () => {
|
|
402
|
+
const mediaFiles = glob.sync(
|
|
403
|
+
path.join(saveTo, "media/**/*.png"),
|
|
404
|
+
);
|
|
405
|
+
expect(mediaFiles).to.be.an("array").that.is.not.empty;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
after(() => {
|
|
409
|
+
fs.removeSync(outputDir);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("rewrite", () => {
|
|
415
|
+
const saveTo = path.join(outputDir, "rewrite-test");
|
|
416
|
+
|
|
417
|
+
beforeEach(() => {
|
|
418
|
+
fs.removeSync(saveTo);
|
|
419
|
+
fs.mkdirpSync(saveTo);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("Should rewrite remote URLs to local media paths", async () => {
|
|
423
|
+
// Create a mock JSON file with remote URLs
|
|
424
|
+
const mockData = JSON.stringify({
|
|
425
|
+
image: "https://example.com/wp-content/uploads/2024/photo.jpg",
|
|
426
|
+
text: "hello world",
|
|
427
|
+
nested: {
|
|
428
|
+
img: "https://example.com/wp-content/uploads/2024/nested.png",
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
|
|
432
|
+
|
|
433
|
+
await rewrite({
|
|
434
|
+
ext: "json",
|
|
435
|
+
type: "wp",
|
|
436
|
+
saveTo,
|
|
437
|
+
bundle: "",
|
|
438
|
+
find: "^https.*/wp-content/uploads",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const result = fs.readFileSync(path.join(saveTo, "test.json"), {
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(result).to.contain("media/content");
|
|
446
|
+
expect(result).to.not.contain("wp-content/uploads");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("Should unwrap contentful response during rewrite", async () => {
|
|
450
|
+
// Contentful responses are arrays, rewrite should extract first element
|
|
451
|
+
const mockData = JSON.stringify([
|
|
452
|
+
{ title: "Entry 1", body: "Content" },
|
|
453
|
+
{ title: "Entry 2", body: "More content" },
|
|
454
|
+
]);
|
|
455
|
+
fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
|
|
456
|
+
|
|
457
|
+
await rewrite({
|
|
458
|
+
ext: "json",
|
|
459
|
+
type: "contentful",
|
|
460
|
+
saveTo,
|
|
461
|
+
bundle: "",
|
|
462
|
+
find: "//images\\.ctfassets\\.net/(.*?)/(.*?)/(.*?)/",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const result = JSON.parse(
|
|
466
|
+
fs.readFileSync(path.join(saveTo, "test.json"), {
|
|
467
|
+
encoding: "utf8",
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Should be unwrapped to first element, not an array
|
|
472
|
+
expect(result).to.have.property("title", "Entry 1");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
after(() => {
|
|
476
|
+
fs.removeSync(saveTo);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe("download", () => {
|
|
481
|
+
const saveTo = path.join(outputDir, "download-test");
|
|
482
|
+
|
|
483
|
+
beforeEach(() => {
|
|
484
|
+
fs.removeSync(saveTo);
|
|
485
|
+
fs.mkdirpSync(saveTo);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("Should extract unique URLs matching find pattern from JSON", async () => {
|
|
489
|
+
// Create JSON that references assets
|
|
490
|
+
const mockData = JSON.stringify({
|
|
491
|
+
images: [
|
|
492
|
+
"https://example.com/wp-content/uploads/photo1.jpg",
|
|
493
|
+
"https://example.com/wp-content/uploads/photo2.jpg",
|
|
494
|
+
"https://example.com/wp-content/uploads/photo1.jpg",
|
|
495
|
+
],
|
|
496
|
+
text: "not a URL",
|
|
497
|
+
});
|
|
498
|
+
fs.writeFileSync(path.join(saveTo, "test.json"), mockData);
|
|
499
|
+
|
|
500
|
+
// download will try to fetch these URLs — they'll fail but
|
|
501
|
+
// we verify the function doesn't throw
|
|
502
|
+
await download({
|
|
503
|
+
path: "https://example.com",
|
|
504
|
+
ext: "json",
|
|
505
|
+
saveTo,
|
|
506
|
+
bundle: "",
|
|
507
|
+
find: "^https.*/wp-content/uploads",
|
|
508
|
+
});
|
|
509
|
+
// If we get here without throwing, the URL extraction and
|
|
510
|
+
// deduplication logic works correctly
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
after(() => {
|
|
514
|
+
fs.removeSync(saveTo);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
});
|