browser-commander 0.26.1 → 0.26.3
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 +12 -0
- package/package.json +1 -1
- package/src/browser/browser-sources.js +13 -0
- package/src/browser/browser-sources.json +13 -0
- package/src/browser/system-browser.js +2 -2
- package/tests/unit/browser/catalogue-launch.test.js +50 -2
- package/tests/unit/scripts/preflight-credentials.test.js +86 -58
- package/tests/unit/scripts/trusted-publishing.test.js +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 796ebd8: Accept dedicated macOS application profiles under `~/Library/Application Support` by separating Safari import discovery roots from protected browser data paths. Safari, Technology Preview and Chrome default profiles remain protected.
|
|
8
|
+
|
|
9
|
+
## 0.26.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- e2ef59f: Verify crates.io trusted publishing before releases and mark repository fixture manifests as non-publishable. Document the account setup required for PyPI and crates.io.
|
|
14
|
+
|
|
3
15
|
## 0.26.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -141,6 +141,19 @@ export function resolveBrowserRoots(name, options = {}) {
|
|
|
141
141
|
return resolveTemplates(templates, options);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Browser-owned paths forbidden as automation profiles. Import discovery may
|
|
146
|
+
* search broader locations; use explicit protection roots when declared.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveBrowserProtectionRoots(name, options = {}) {
|
|
149
|
+
const source = normalizeBrowserSource(name);
|
|
150
|
+
const roots = source.protectionRoots ?? source.roots;
|
|
151
|
+
return resolveTemplates(
|
|
152
|
+
roots?.[options.platform ?? process.platform] ?? [],
|
|
153
|
+
options
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
144
157
|
function resolveTemplates(
|
|
145
158
|
templates,
|
|
146
159
|
{
|
|
@@ -764,6 +764,13 @@
|
|
|
764
764
|
"{home}/Library"
|
|
765
765
|
]
|
|
766
766
|
},
|
|
767
|
+
"protectionRoots": {
|
|
768
|
+
"darwin": [
|
|
769
|
+
"{home}/Library/Safari",
|
|
770
|
+
"{home}/Library/Containers/com.apple.Safari",
|
|
771
|
+
"{home}/Library/Cookies"
|
|
772
|
+
]
|
|
773
|
+
},
|
|
767
774
|
"default": {
|
|
768
775
|
"darwin": ["com.apple.safari"]
|
|
769
776
|
},
|
|
@@ -781,6 +788,12 @@
|
|
|
781
788
|
"{home}/Library/Safari Technology Preview"
|
|
782
789
|
]
|
|
783
790
|
},
|
|
791
|
+
"protectionRoots": {
|
|
792
|
+
"darwin": [
|
|
793
|
+
"{home}/Library/Safari Technology Preview",
|
|
794
|
+
"{home}/Library/Containers/com.apple.SafariTechnologyPreview"
|
|
795
|
+
]
|
|
796
|
+
},
|
|
784
797
|
"default": {
|
|
785
798
|
"darwin": ["com.apple.safaritechnologypreview"]
|
|
786
799
|
},
|
|
@@ -2,7 +2,7 @@ import { constants } from 'node:fs';
|
|
|
2
2
|
import { physicalPath } from './browser-profile-files.js';
|
|
3
3
|
import {
|
|
4
4
|
BROWSER_SOURCES,
|
|
5
|
-
|
|
5
|
+
resolveBrowserProtectionRoots,
|
|
6
6
|
resolveBrowserExecutables,
|
|
7
7
|
} from './browser-sources.js';
|
|
8
8
|
import { access } from 'node:fs/promises';
|
|
@@ -46,7 +46,7 @@ export function knownDefaultUserDataDirs({
|
|
|
46
46
|
environment = process.env,
|
|
47
47
|
} = {}) {
|
|
48
48
|
return BROWSER_SOURCES.flatMap((browser) =>
|
|
49
|
-
|
|
49
|
+
resolveBrowserProtectionRoots(browser.id, {
|
|
50
50
|
platform,
|
|
51
51
|
homeDir,
|
|
52
52
|
environment,
|
|
@@ -6,6 +6,7 @@ import { useTempDirectories } from '../../helpers/temp-directory.js';
|
|
|
6
6
|
import {
|
|
7
7
|
BROWSER_SOURCES,
|
|
8
8
|
resolveBrowserRoots,
|
|
9
|
+
resolveBrowserProtectionRoots,
|
|
9
10
|
} from '../../../src/browser/browser-sources.js';
|
|
10
11
|
import {
|
|
11
12
|
assertDedicatedUserDataDir,
|
|
@@ -13,8 +14,51 @@ import {
|
|
|
13
14
|
} from '../../../src/browser/system-browser.js';
|
|
14
15
|
|
|
15
16
|
const temporary = useTempDirectories('bc-catalogue-protection-');
|
|
17
|
+
const macOSOptions = {
|
|
18
|
+
platform: 'darwin',
|
|
19
|
+
homeDir: '/users/test',
|
|
20
|
+
environment: {},
|
|
21
|
+
};
|
|
16
22
|
|
|
17
23
|
describe('catalogue launch and protection (#121)', () => {
|
|
24
|
+
it('accepts dedicated macOS application profiles under Library (#132)', () => {
|
|
25
|
+
const options = macOSOptions;
|
|
26
|
+
for (const directory of [
|
|
27
|
+
'Library/Application Support/package-registry-manager/browser-profile',
|
|
28
|
+
'Library/Safari-backup/profile',
|
|
29
|
+
'Library/Cookies-backup/profile',
|
|
30
|
+
'Library/Containers/com.apple.Safari-helper/profile',
|
|
31
|
+
]) {
|
|
32
|
+
assert.doesNotThrow(() =>
|
|
33
|
+
assertDedicatedUserDataDir(
|
|
34
|
+
path.posix.join(options.homeDir, directory),
|
|
35
|
+
options
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('protects Safari stores and Chrome profiles on macOS (#132)', () => {
|
|
42
|
+
const options = macOSOptions;
|
|
43
|
+
for (const directory of [
|
|
44
|
+
'Library/Safari',
|
|
45
|
+
'Library/Cookies',
|
|
46
|
+
'Library/Containers/com.apple.Safari',
|
|
47
|
+
'Library/Safari Technology Preview',
|
|
48
|
+
'Library/Containers/com.apple.SafariTechnologyPreview',
|
|
49
|
+
'Library/Application Support/Google/Chrome/Default',
|
|
50
|
+
]) {
|
|
51
|
+
const root = path.posix.join(options.homeDir, directory);
|
|
52
|
+
for (const requested of [root, path.posix.join(root, 'new-profile')]) {
|
|
53
|
+
assert.throws(
|
|
54
|
+
() => assertDedicatedUserDataDir(requested, options),
|
|
55
|
+
/dedicated.*default profile/,
|
|
56
|
+
requested
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
18
62
|
it('protects a new profile beneath a symlink to a default root', async () => {
|
|
19
63
|
const homeDir = await temporary();
|
|
20
64
|
const options = { homeDir, environment: {} };
|
|
@@ -47,7 +91,7 @@ describe('catalogue launch and protection (#121)', () => {
|
|
|
47
91
|
}
|
|
48
92
|
});
|
|
49
93
|
|
|
50
|
-
it('protects every catalogue root and descendant profile on every OS', () => {
|
|
94
|
+
it('protects every catalogue protection root and descendant profile on every OS', () => {
|
|
51
95
|
for (const platform of ['linux', 'darwin', 'win32']) {
|
|
52
96
|
const options = {
|
|
53
97
|
platform,
|
|
@@ -55,7 +99,11 @@ describe('catalogue launch and protection (#121)', () => {
|
|
|
55
99
|
environment: {},
|
|
56
100
|
};
|
|
57
101
|
for (const browser of BROWSER_SOURCES) {
|
|
58
|
-
|
|
102
|
+
const roots = resolveBrowserProtectionRoots(browser.id, options);
|
|
103
|
+
if (!browser.protectionRoots) {
|
|
104
|
+
assert.deepEqual(roots, resolveBrowserRoots(browser.id, options));
|
|
105
|
+
}
|
|
106
|
+
for (const root of roots) {
|
|
59
107
|
assert.throws(
|
|
60
108
|
() => assertDedicatedUserDataDir(root, options),
|
|
61
109
|
/dedicated.*default profile/,
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Run 37509328334 built, tested and versioned the Python package on main and
|
|
6
6
|
* only then failed at "Publish to PyPI" with `invalid-publisher` (issue #128).
|
|
7
7
|
* `scripts/preflight-credentials.sh` performs the credential half of each
|
|
8
|
-
* publish up front -- the OIDC
|
|
9
|
-
*
|
|
8
|
+
* publish up front -- the OIDC exchanges all three registries do -- and the
|
|
9
|
+
* `release-preflight` job in each
|
|
10
10
|
* language workflow gates the publishing jobs on it. These tests pin the rules
|
|
11
11
|
* the workflows depend on (principle 16 of the link-foundation pipeline
|
|
12
12
|
* templates): every failure is reported, `unknown` is never a pass in release
|
|
@@ -28,6 +28,7 @@ import { after, before, beforeEach, describe, it } from 'node:test';
|
|
|
28
28
|
import { BASH_AVAILABLE, readRepoText, repoPath } from '../../helpers/repo.js';
|
|
29
29
|
|
|
30
30
|
const SCRIPT = repoPath('scripts/preflight-credentials.sh');
|
|
31
|
+
const scriptOptions = { skip: !BASH_AVAILABLE, timeout: 30_000 };
|
|
31
32
|
|
|
32
33
|
// Values a leak would make visible: none of them may reach the output.
|
|
33
34
|
const REQUEST_TOKEN = 'gh-request-token-must-not-leak';
|
|
@@ -130,7 +131,8 @@ const OIDC_OK = [200, { value: OIDC_TOKEN }];
|
|
|
130
131
|
const MINT = 'POST /pypi/_/oidc/mint-token';
|
|
131
132
|
const EXCHANGE =
|
|
132
133
|
'POST /npm/-/npm/v1/oidc/token/exchange/package/@link-foundation%2Fprobe';
|
|
133
|
-
const
|
|
134
|
+
const CRATES_MINT = 'POST /crates/api/v1/trusted_publishing/tokens';
|
|
135
|
+
const CRATES_REVOKE = 'DELETE /crates/api/v1/trusted_publishing/tokens';
|
|
134
136
|
const INVALID_PUBLISHER = [
|
|
135
137
|
422,
|
|
136
138
|
{
|
|
@@ -150,7 +152,12 @@ function probePypi(answer, overrides = {}) {
|
|
|
150
152
|
return preflight({ PREFLIGHT_REGISTRIES: 'pypi', ...overrides });
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
function probeCrates(answer, overrides = {}) {
|
|
156
|
+
stub.routes[CRATES_MINT] = answer;
|
|
157
|
+
return preflight({ PREFLIGHT_REGISTRIES: 'crates', ...overrides });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
describe('scripts/preflight-credentials.sh', scriptOptions, () => {
|
|
154
161
|
before(async () => {
|
|
155
162
|
stub.server = createServer(respond);
|
|
156
163
|
stub.server.listen(0, '127.0.0.1');
|
|
@@ -318,81 +325,102 @@ describe('scripts/preflight-credentials.sh', { skip: !BASH_AVAILABLE }, () => {
|
|
|
318
325
|
});
|
|
319
326
|
});
|
|
320
327
|
|
|
321
|
-
describe('crates.io
|
|
322
|
-
it('
|
|
323
|
-
stub.routes[
|
|
324
|
-
403,
|
|
325
|
-
{
|
|
326
|
-
errors: [
|
|
327
|
-
{
|
|
328
|
-
detail:
|
|
329
|
-
'this action can only be performed on the crates.io website',
|
|
330
|
-
},
|
|
331
|
-
],
|
|
332
|
-
},
|
|
333
|
-
];
|
|
328
|
+
describe('crates.io trusted publishing', () => {
|
|
329
|
+
it('exchanges OIDC and revokes the temporary token without using a Cargo secret', async () => {
|
|
330
|
+
stub.routes[CRATES_REVOKE] = [204, ''];
|
|
334
331
|
|
|
335
|
-
const { status, output } = await
|
|
336
|
-
|
|
337
|
-
CARGO_REGISTRY_TOKEN: CARGO_SECRET
|
|
338
|
-
|
|
332
|
+
const { status, output } = await probeCrates(
|
|
333
|
+
[200, { token: ISSUED_TOKEN }],
|
|
334
|
+
{ CARGO_REGISTRY_TOKEN: CARGO_SECRET }
|
|
335
|
+
);
|
|
339
336
|
|
|
340
337
|
assert.equal(status, 0, output);
|
|
341
|
-
assert.match(
|
|
342
|
-
|
|
343
|
-
|
|
338
|
+
assert.match(
|
|
339
|
+
output,
|
|
340
|
+
/PASS: crates.io minted a short-lived publish token/
|
|
341
|
+
);
|
|
342
|
+
const [oidc, mint, revoke] = stub.requests;
|
|
343
|
+
assert.match(oidc.url, /&audience=127\.0\.0\.1:\d+\/crates$/);
|
|
344
|
+
assert.deepEqual(JSON.parse(mint.body), { jwt: OIDC_TOKEN });
|
|
345
|
+
assert.equal(mint.authorization, '');
|
|
346
|
+
assert.equal(revoke.route, CRATES_REVOKE);
|
|
347
|
+
assert.equal(revoke.authorization, `Bearer ${ISSUED_TOKEN}`);
|
|
348
|
+
assert.equal(stub.requests.length, 3);
|
|
344
349
|
assertNoSecrets(output);
|
|
345
350
|
});
|
|
346
351
|
|
|
347
|
-
it('fails a release
|
|
348
|
-
|
|
349
|
-
403,
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const { status, output } = await preflight({
|
|
354
|
-
PREFLIGHT_REGISTRIES: 'crates',
|
|
355
|
-
CARGO_TOKEN: CARGO_SECRET,
|
|
356
|
-
});
|
|
352
|
+
it('fails a release when no trusted publisher matches, even with a legacy secret', async () => {
|
|
353
|
+
const { status, output } = await probeCrates(
|
|
354
|
+
[403, { errors: [{ detail: 'no matching publisher' }] }],
|
|
355
|
+
{ CARGO_TOKEN: CARGO_SECRET }
|
|
356
|
+
);
|
|
357
357
|
|
|
358
358
|
assert.equal(status, 1, output);
|
|
359
359
|
assert.match(
|
|
360
360
|
output,
|
|
361
|
-
/::error::release-preflight: crates.io
|
|
361
|
+
/::error::release-preflight: crates.io refused the OIDC token exchange \(403\)/
|
|
362
362
|
);
|
|
363
|
+
assert.match(output, /rust\.yml/);
|
|
364
|
+
assert.match(output, /docs\/PUBLISHING\.md/);
|
|
365
|
+
assert.equal(stub.requests.length, 2);
|
|
363
366
|
assertNoSecrets(output);
|
|
364
367
|
});
|
|
365
368
|
|
|
366
|
-
it('fails when no
|
|
369
|
+
it('fails when the job has no OIDC permission, with no secret fallback', async () => {
|
|
367
370
|
const { status, output } = await preflight({
|
|
368
371
|
PREFLIGHT_REGISTRIES: 'crates',
|
|
372
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: '',
|
|
373
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: '',
|
|
374
|
+
CARGO_REGISTRY_TOKEN: CARGO_SECRET,
|
|
369
375
|
});
|
|
370
376
|
|
|
371
377
|
assert.equal(status, 1, output);
|
|
372
|
-
assert.match(
|
|
378
|
+
assert.match(
|
|
379
|
+
output,
|
|
380
|
+
/crates.io: no OIDC token available: .*id-token: write/
|
|
381
|
+
);
|
|
382
|
+
assert.equal(stub.requests.length, 0);
|
|
373
383
|
});
|
|
374
384
|
|
|
375
|
-
it('
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
PREFLIGHT_REGISTRIES: 'crates',
|
|
384
|
-
CARGO_REGISTRY_TOKEN: CARGO_SECRET,
|
|
385
|
-
};
|
|
385
|
+
it('keeps missing publishers advisory on pull requests', async () => {
|
|
386
|
+
const { status, output } = await probeCrates([403, {}], {
|
|
387
|
+
PREFLIGHT_MODE: 'report',
|
|
388
|
+
});
|
|
389
|
+
assert.equal(status, 0, output);
|
|
390
|
+
assert.match(output, /::warning::release-preflight: crates.io refused/);
|
|
391
|
+
assert.doesNotMatch(output, /::error::/);
|
|
392
|
+
});
|
|
386
393
|
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
394
|
+
it('treats missing tokens and unavailable registries as unknown', async () => {
|
|
395
|
+
for (const answer of [
|
|
396
|
+
[200, {}],
|
|
397
|
+
[429, {}],
|
|
398
|
+
[503, {}],
|
|
399
|
+
]) {
|
|
400
|
+
const { status, output } = await probeCrates(answer);
|
|
401
|
+
assert.equal(status, 1, output);
|
|
402
|
+
assert.match(output, /0 verified, 0 failed, 1 unknown/);
|
|
403
|
+
assert.equal(stub.requests.at(-1).route, CRATES_MINT);
|
|
404
|
+
assertNoSecrets(output);
|
|
405
|
+
}
|
|
406
|
+
const port = await closedPort();
|
|
407
|
+
const { status, output } = await preflight({
|
|
408
|
+
PREFLIGHT_REGISTRIES: 'crates',
|
|
409
|
+
CRATES_API: `http://127.0.0.1:${port}`,
|
|
410
|
+
});
|
|
411
|
+
assert.equal(status, 1, output);
|
|
412
|
+
assert.match(output, /UNKNOWN: crates.io was unreachable/);
|
|
413
|
+
});
|
|
391
414
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
415
|
+
it('reports failed revocation without exposing the token', async () => {
|
|
416
|
+
stub.routes[CRATES_REVOKE] = [403, { token: ISSUED_TOKEN }];
|
|
417
|
+
const { status, output } = await probeCrates([
|
|
418
|
+
200,
|
|
419
|
+
{ token: ISSUED_TOKEN },
|
|
420
|
+
]);
|
|
421
|
+
assert.equal(status, 1, output);
|
|
422
|
+
assert.match(output, /could not revoke the preflight token/);
|
|
423
|
+
assertNoSecrets(output);
|
|
396
424
|
});
|
|
397
425
|
});
|
|
398
426
|
|
|
@@ -400,7 +428,7 @@ describe('scripts/preflight-credentials.sh', { skip: !BASH_AVAILABLE }, () => {
|
|
|
400
428
|
it('reports every failure rather than stopping at the first', async () => {
|
|
401
429
|
stub.routes[MINT] = INVALID_PUBLISHER;
|
|
402
430
|
stub.routes[EXCHANGE] = [403, { message: 'forbidden' }];
|
|
403
|
-
stub.routes[
|
|
431
|
+
stub.routes[CRATES_MINT] = [401, { errors: [{ detail: 'bad token' }] }];
|
|
404
432
|
const summary = path.join(workDir, 'summary.md');
|
|
405
433
|
writeFileSync(summary, '');
|
|
406
434
|
|
|
@@ -479,7 +507,7 @@ const PREFLIGHT_WORKFLOWS = [
|
|
|
479
507
|
{
|
|
480
508
|
file: 'rust.yml',
|
|
481
509
|
registry: 'crates',
|
|
482
|
-
oidc:
|
|
510
|
+
oidc: true,
|
|
483
511
|
publishers: ['auto-release', 'manual-release'],
|
|
484
512
|
},
|
|
485
513
|
];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
|
|
5
|
+
import { readRepoText } from '../../helpers/repo.js';
|
|
6
|
+
|
|
7
|
+
describe('trusted publishing configuration', () => {
|
|
8
|
+
const rust = parse(readRepoText('.github/workflows/rust.yml'));
|
|
9
|
+
|
|
10
|
+
for (const jobId of ['auto-release', 'manual-release']) {
|
|
11
|
+
it(`${jobId} uses a fresh OIDC token only when publishing`, () => {
|
|
12
|
+
const job = rust.jobs[jobId];
|
|
13
|
+
assert.equal(job.permissions['id-token'], 'write');
|
|
14
|
+
const auth = job.steps.find((step) => step.id === 'crates_auth');
|
|
15
|
+
const publish = job.steps.find((step) => step.id === 'publish-crate');
|
|
16
|
+
assert.ok(auth, 'missing crates.io authentication step');
|
|
17
|
+
assert.match(
|
|
18
|
+
auth.uses,
|
|
19
|
+
/^rust-lang\/crates-io-auth-action@[a-f0-9]{40}$/
|
|
20
|
+
);
|
|
21
|
+
assert.equal(auth.if, publish.if);
|
|
22
|
+
assert.equal(
|
|
23
|
+
publish.env.CARGO_REGISTRY_TOKEN,
|
|
24
|
+
'${{ steps.crates_auth.outputs.token }}'
|
|
25
|
+
);
|
|
26
|
+
assert.ok(job.steps.indexOf(auth) < job.steps.indexOf(publish));
|
|
27
|
+
assert.ok(
|
|
28
|
+
job.steps.indexOf(auth) >
|
|
29
|
+
job.steps.findIndex((step) => step.name === 'Build release')
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it('never reads the long-lived Cargo secret in the Rust workflow', () => {
|
|
35
|
+
assert.doesNotMatch(
|
|
36
|
+
readRepoText('.github/workflows/rust.yml'),
|
|
37
|
+
/secrets\.CARGO_TOKEN/
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('keeps the registered PyPI identity identical across preflight and releases', () => {
|
|
42
|
+
const python = parse(readRepoText('.github/workflows/python.yml'));
|
|
43
|
+
for (const jobId of [
|
|
44
|
+
'release-preflight',
|
|
45
|
+
'auto-release',
|
|
46
|
+
'manual-release',
|
|
47
|
+
]) {
|
|
48
|
+
assert.equal(python.jobs[jobId].permissions['id-token'], 'write');
|
|
49
|
+
assert.equal(python.jobs[jobId].environment, undefined);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('marks the template snapshot as private without hiding the real npm package', () => {
|
|
54
|
+
assert.equal(
|
|
55
|
+
JSON.parse(
|
|
56
|
+
readRepoText(
|
|
57
|
+
'docs/case-studies/issue-55/template-snapshots/js/package.json'
|
|
58
|
+
)
|
|
59
|
+
).private,
|
|
60
|
+
true
|
|
61
|
+
);
|
|
62
|
+
assert.notEqual(JSON.parse(readRepoText('js/package.json')).private, true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('disables publishing for the cookie equivalence experiment only', () => {
|
|
66
|
+
const fixture = readRepoText(
|
|
67
|
+
'experiments/issue-128/cookie-fixture-equivalence/Cargo.toml'
|
|
68
|
+
);
|
|
69
|
+
const packageTable = fixture
|
|
70
|
+
.split(/^\[package\]\s*$/m)[1]
|
|
71
|
+
?.split(/^\[/m)[0];
|
|
72
|
+
assert.match(packageTable ?? '', /^publish\s*=\s*false\s*$/m);
|
|
73
|
+
assert.doesNotMatch(
|
|
74
|
+
readRepoText('rust/Cargo.toml'),
|
|
75
|
+
/^publish\s*=\s*false\s*$/m
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|