browser-commander 0.24.0 → 0.25.0

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.
Files changed (60) hide show
  1. package/.jscpd-baseline.json +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +7 -4
  4. package/package.json +4 -1
  5. package/src/browser/attach/snapshot.js +4 -28
  6. package/src/browser/browser-cookie-credentials.js +15 -8
  7. package/src/browser/browser-cookies.js +36 -30
  8. package/src/browser/browser-profile-files.js +31 -0
  9. package/src/browser/browser-profiles.js +11 -13
  10. package/src/browser/browser-sources.js +39 -9
  11. package/src/browser/browser-sources.json +600 -23
  12. package/src/browser/migration/capabilities.json +117 -0
  13. package/src/browser/migration/chromium-history.sql +22 -0
  14. package/src/browser/migration/chromium-writers.js +85 -0
  15. package/src/browser/migration/cookies.js +4 -1
  16. package/src/browser/migration/data-classes.js +68 -0
  17. package/src/browser/migration/domains.js +32 -0
  18. package/src/browser/migration/firefox-history.js +106 -0
  19. package/src/browser/migration/firefox.js +39 -25
  20. package/src/browser/migration/history.js +161 -2
  21. package/src/browser/migration/index.js +229 -83
  22. package/src/browser/migration/os-crypt-keys.js +1 -5
  23. package/src/browser/migration/password-metadata.js +95 -0
  24. package/src/browser/migration/passwords.js +78 -41
  25. package/src/browser/migration/safari-import.js +100 -0
  26. package/src/browser/migration/safari.js +110 -0
  27. package/src/browser/migration/sqlite-snapshot.js +31 -65
  28. package/src/browser/migration/validation.js +48 -0
  29. package/src/browser/real-browser.js +13 -1
  30. package/src/browser/safari-access.js +45 -0
  31. package/src/browser/safari-cookies.js +39 -36
  32. package/src/browser/safari-profiles.js +83 -0
  33. package/src/browser/system-browser.js +31 -143
  34. package/src/cli/args.js +2 -0
  35. package/src/cli/commands.js +2 -0
  36. package/src/cli/high-level-methods.js +2 -0
  37. package/tests/unit/browser/attach/snapshot.test.js +30 -0
  38. package/tests/unit/browser/browser-cookies.test.js +35 -0
  39. package/tests/unit/browser/catalogue-launch.test.js +93 -0
  40. package/tests/unit/browser/cookie-sources.test.js +92 -17
  41. package/tests/unit/browser/credential-catalogue.test.js +50 -0
  42. package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
  43. package/tests/unit/browser/migration/data-classes.test.js +89 -0
  44. package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
  45. package/tests/unit/browser/migration/firefox-history.test.js +130 -0
  46. package/tests/unit/browser/migration/history-metadata.test.js +74 -0
  47. package/tests/unit/browser/migration/index.test.js +16 -1
  48. package/tests/unit/browser/migration/opera-keys.test.js +22 -0
  49. package/tests/unit/browser/migration/safari-data.test.js +143 -0
  50. package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
  51. package/tests/unit/browser/migration/validation.test.js +63 -0
  52. package/tests/unit/browser/migration/yandex.test.js +84 -0
  53. package/tests/unit/browser/real-browser.test.js +19 -0
  54. package/tests/unit/browser/safari-cookies.test.js +26 -1
  55. package/tests/unit/browser/safari-profiles.test.js +111 -0
  56. package/tests/unit/cli/dispatcher.test.js +4 -0
  57. package/tests/unit/cli/main.test.js +5 -0
  58. package/tests/unit/scripts/browser-support.test.js +14 -7
  59. package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
  60. package/tests/unit/scripts/migration-support.test.js +31 -0
@@ -49,6 +49,22 @@ async function installed(browser = 'safari', legacy = false) {
49
49
  }
50
50
 
51
51
  describe('Safari cookies', { timeout: 10000 }, () => {
52
+ it('uses whole domains for discovery while retaining reader substring filters', async () => {
53
+ const options = await installed();
54
+ const sources = await listCookieSources({
55
+ ...options,
56
+ domains: ['hub.com', 'github.co'],
57
+ });
58
+ assert.deepEqual(sources, []);
59
+ assert.deepEqual(
60
+ await readBrowserCookiesWithDependencies(
61
+ { browser: 'safari', domainFilter: 'hub.com', cache: false },
62
+ options
63
+ ),
64
+ expected.slice(0, 2)
65
+ );
66
+ });
67
+
52
68
  it('decodes multiple pages, flags, UTF-8 and Apple epoch without guessing session cookies', () => {
53
69
  assert.deepEqual(parseSafariCookies(fixture), expected);
54
70
  });
@@ -133,7 +149,16 @@ describe('Safari cookies', { timeout: 10000 }, () => {
133
149
  });
134
150
  assert.deepEqual(report.cookies, expected.slice(0, 2));
135
151
  assert.equal(report.migrated.cookies, 2);
136
- assert.equal(report.skipped.length, 5);
152
+ const classes = JSON.parse(
153
+ await readFile(
154
+ repoPath('tests/fixtures/migration-data-classes.json'),
155
+ 'utf8'
156
+ )
157
+ );
158
+ assert.deepEqual(
159
+ new Set(report.skipped.map((entry) => entry.type)),
160
+ new Set(classes.filter((type) => type !== 'cookies'))
161
+ );
137
162
  assert.equal(
138
163
  report.skipped.find((entry) => entry.type === 'passwords').reason,
139
164
  'safari-password-export-required'
@@ -0,0 +1,111 @@
1
+ import assert from 'node:assert/strict';
2
+ import { copyFile, mkdir, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { it } from 'node:test';
5
+ import Database from 'better-sqlite3';
6
+ import { listBrowserProfiles } from '../../../src/browser/browser-profiles.js';
7
+ import { listCookieSources } from '../../../src/browser/browser-cookies.js';
8
+ import { findSafariCookieFile } from '../../../src/browser/safari-cookies.js';
9
+ import { repoPath } from '../../helpers/repo.js';
10
+ import { useTempDirectories } from '../../helpers/temp-directory.js';
11
+
12
+ const temporary = useTempDirectories('bc-safari-stores-');
13
+ const uuid = '11111111-2222-3333-4444-555555555555';
14
+
15
+ it('discovers Safari named profiles and prefers WebKit stores to stale legacy cookies', async () => {
16
+ const homeDir = await temporary();
17
+ const root = path.join(
18
+ homeDir,
19
+ 'Library/Containers/com.apple.Safari/Data/Library'
20
+ );
21
+ const named = path.join(root, 'Safari/Profiles', uuid);
22
+ await mkdir(named, { recursive: true });
23
+ const db = new Database(path.join(root, 'Safari/SafariTabs.db'));
24
+ db.exec(
25
+ await readFile(
26
+ repoPath('tests/fixtures/safari-data/SafariTabs.sql'),
27
+ 'utf8'
28
+ )
29
+ );
30
+ db.close();
31
+ const locations = [
32
+ 'Cookies',
33
+ 'WebKit/WebsiteData/Default/Cookies',
34
+ `WebKit/WebsiteDataStore/${uuid}/Cookies`,
35
+ ];
36
+ for (const location of locations) {
37
+ await mkdir(path.join(root, location), { recursive: true });
38
+ await copyFile(
39
+ repoPath('tests/fixtures/safari/Cookies.binarycookies'),
40
+ path.join(root, location, 'Cookies.binarycookies')
41
+ );
42
+ }
43
+ const profiles = await listBrowserProfiles({
44
+ browser: 'safari',
45
+ platform: 'darwin',
46
+ homeDir,
47
+ environment: {},
48
+ });
49
+ assert.deepEqual(
50
+ profiles.map(({ name }) => name),
51
+ ['Default', uuid]
52
+ );
53
+ assert.equal(profiles[1].displayName, 'Work');
54
+ assert.equal(profiles[1].path, named);
55
+ assert.equal(
56
+ await findSafariCookieFile(root),
57
+ path.join(root, locations[1], 'Cookies.binarycookies')
58
+ );
59
+ assert.equal(
60
+ await findSafariCookieFile(named),
61
+ path.join(root, locations[2], 'Cookies.binarycookies')
62
+ );
63
+ });
64
+
65
+ it('preserves a Safari catalogue error while listing its readable cookies and other browsers', async () => {
66
+ const homeDir = await temporary();
67
+ const root = path.join(
68
+ homeDir,
69
+ 'Library/Containers/com.apple.Safari/Data/Library'
70
+ );
71
+ await mkdir(path.join(root, 'Safari'), { recursive: true });
72
+ await mkdir(path.join(root, 'Cookies'));
73
+ await copyFile(
74
+ repoPath('tests/fixtures/safari/Cookies.binarycookies'),
75
+ path.join(root, 'Cookies/Cookies.binarycookies')
76
+ );
77
+ const tabs = path.join(root, 'Safari/SafariTabs.db');
78
+ new Database(tabs).close();
79
+ const chromium = path.join(
80
+ homeDir,
81
+ 'Library/Application Support/Google/Chrome/Default'
82
+ );
83
+ await mkdir(chromium, { recursive: true });
84
+ const db = new Database(path.join(chromium, 'Cookies'));
85
+ db.exec(
86
+ "CREATE TABLE cookies(host_key TEXT); INSERT INTO cookies VALUES ('.github.com')"
87
+ );
88
+ db.close();
89
+ const before = await readFile(tabs);
90
+ const options = { platform: 'darwin', homeDir, environment: {} };
91
+ const profiles = await listBrowserProfiles(options);
92
+ assert.equal(
93
+ profiles.filter(({ browser }) => browser === 'safari').length,
94
+ 2
95
+ );
96
+ const sources = await listCookieSources({
97
+ ...options,
98
+ domains: ['github.com'],
99
+ });
100
+ assert.equal(sources.find(({ browser }) => browser === 'chrome').cookies, 1);
101
+ assert.equal(
102
+ sources.find(({ browser, error }) => browser === 'safari' && !error)
103
+ .cookies,
104
+ 4
105
+ );
106
+ assert.match(
107
+ sources.find(({ browser, error }) => browser === 'safari' && error).error,
108
+ /no such table/u
109
+ );
110
+ assert.deepEqual(await readFile(tabs), before);
111
+ });
@@ -182,8 +182,12 @@ describe('dispatcher: high-level methods', () => {
182
182
  const migrated = await dispatch('profile.migrate', {
183
183
  from: 'chrome',
184
184
  include: ['cookies'],
185
+ passwordCsv: '/tmp/export.csv',
186
+ includePaymentCards: true,
185
187
  });
186
188
  assert.deepEqual(migrated.options.include, ['cookies']);
189
+ assert.equal(migrated.options.passwordCsv, '/tmp/export.csv');
190
+ assert.equal(migrated.options.includePaymentCards, true);
187
191
  assert.deepEqual(await dispatch('doctor', {}), { ok: true, unlisted: [] });
188
192
  assert.equal((await dispatch('version')).language, 'js');
189
193
  });
@@ -247,6 +247,9 @@ describe('runCli', () => {
247
247
  'cookies, bookmarks',
248
248
  '--domain',
249
249
  'a.test',
250
+ '--password-csv',
251
+ '/tmp/export.csv',
252
+ '--include-payment-cards',
250
253
  ]);
251
254
 
252
255
  // The source browser is grouped under `from` so migrateProfile receives
@@ -254,6 +257,8 @@ describe('runCli', () => {
254
257
  assert.equal(document.options.from.browser, 'chrome');
255
258
  assert.deepEqual(document.options.include, ['cookies', 'bookmarks']);
256
259
  assert.deepEqual(document.options.domains, ['a.test']);
260
+ assert.equal(document.options.passwordCsv, '/tmp/export.csv');
261
+ assert.equal(document.options.includePaymentCards, true);
257
262
  });
258
263
 
259
264
  it('lists cookie sources with per-domain counts', async () => {
@@ -16,12 +16,13 @@ import { pathToFileURL } from 'node:url';
16
16
 
17
17
  import { repoPath } from '../../helpers/repo.js';
18
18
 
19
- const { BEGIN_MARKER, END_MARKER, readCatalogue, renderBrowserSupport } =
20
- await import(pathToFileURL(repoPath('scripts/generate-browser-support.mjs')));
19
+ const generator = await import(
20
+ pathToFileURL(repoPath('scripts/generate-browser-support.mjs'))
21
+ );
21
22
 
22
23
  describe('generate-browser-support', () => {
23
24
  it('renders a row per browser with per-platform availability', () => {
24
- const table = renderBrowserSupport([
25
+ const table = generator.renderBrowserSupport([
25
26
  {
26
27
  id: 'chrome',
27
28
  family: 'chromium',
@@ -47,10 +48,16 @@ describe('generate-browser-support', () => {
47
48
  const readme = fs
48
49
  .readFileSync(repoPath('README.md'), 'utf8')
49
50
  .replaceAll('\r\n', '\n');
50
- const begin = readme.indexOf(BEGIN_MARKER);
51
- const end = readme.indexOf(END_MARKER);
51
+ const begin = readme.indexOf(generator.BEGIN_MARKER);
52
+ const end = readme.indexOf(generator.END_MARKER);
52
53
  assert.ok(begin !== -1 && end > begin, 'README is missing the markers');
53
- const between = readme.slice(begin + BEGIN_MARKER.length, end).trim();
54
- assert.equal(between, renderBrowserSupport(readCatalogue(repoPath('.'))));
54
+ const between = readme
55
+ .slice(begin + generator.BEGIN_MARKER.length, end)
56
+ .trim();
57
+ const browsers = generator.readCatalogue(repoPath('.'));
58
+ assert.equal(
59
+ between,
60
+ `${generator.renderBrowserSupport(browsers)}\n\n${generator.renderBrowserControls(browsers)}`
61
+ );
55
62
  });
56
63
  });
@@ -5,10 +5,12 @@
5
5
  * *failed*, and a run whose only casualty is a cancelled job is filed under
6
6
  * `cancelled` as well - run 24045269874 of this repository is one such run on
7
7
  * `main`. scripts/check-pipeline-status.sh is the only thing that looks at
8
- * that, so these tests pin the four readings it has to get right: a failure is
8
+ * that, so these tests pin the readings it has to get right: a failure is
9
9
  * always an error, a cancellation off the default branch is a warning, a
10
10
  * cancellation on the default branch is an error unless a newer run has already
11
11
  * taken over the branch head.
12
+ * Hosted-runner failures can report an undocumented abandoned result; it and
13
+ * other unexpected/missing results must fail on every branch.
12
14
  *
13
15
  * See docs/CI-TIMEOUT-BUDGETS.md.
14
16
  */
@@ -66,6 +68,36 @@ describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
66
68
  assert.match(output, /::error::Pipeline failed\. Failing jobs: lint/);
67
69
  });
68
70
 
71
+ for (const env of [
72
+ { IS_MAIN: 'false' },
73
+ { IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: 'def' },
74
+ ]) {
75
+ it(`fails on an abandoned runner job with IS_MAIN=${env.IS_MAIN}`, () => {
76
+ // Hosted-runner acquisition failure in run 37362314527 left the macOS
77
+ // check cancelled, but GitHub sent "abandoned" to the needs context.
78
+ const { status, output } = runGate(
79
+ results({ lint: 'success', test: 'abandoned', release: 'skipped' }),
80
+ env
81
+ );
82
+
83
+ assert.equal(status, 1);
84
+ assert.match(output, /::error::Pipeline has unexpected job results/);
85
+ assert.match(output, /test \(abandoned\)/);
86
+ assert.doesNotMatch(output, /All required jobs succeeded/);
87
+ });
88
+ }
89
+
90
+ for (const result of ['timed_out', null, undefined]) {
91
+ it(`rejects an unexpected result of ${result}`, () => {
92
+ const { status, output } = runGate(results({ test: result }));
93
+
94
+ assert.equal(status, 1);
95
+ assert.match(output, /::error::Pipeline has unexpected job results/);
96
+ assert.match(output, /test \(/);
97
+ assert.doesNotMatch(output, /All required jobs succeeded/);
98
+ });
99
+ }
100
+
69
101
  it('only warns about a cancellation off the default branch', () => {
70
102
  const { status, output } = runGate(results({ test: 'cancelled' }), {
71
103
  IS_MAIN: 'false',
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import { it } from 'node:test';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { repoPath } from '../../helpers/repo.js';
6
+ import { ALL_DATA_CLASSES } from '../../../src/browser/migration/index.js';
7
+
8
+ const { BEGIN_MARKER, END_MARKER, readCapabilities, renderMigrationSupport } =
9
+ await import(
10
+ pathToFileURL(repoPath('scripts/generate-migration-support.mjs'))
11
+ );
12
+ it('generates each source/target/class cell and declares unsupported target writers', () => {
13
+ const capabilities = readCapabilities(repoPath('.'));
14
+ assert.deepEqual(ALL_DATA_CLASSES, capabilities.classes);
15
+ const table = renderMigrationSupport(capabilities);
16
+ assert.equal(table.split('\n').length, 11);
17
+ assert.match(table, /safari\s+\| chromium\s+\| seed\s+\| translate/u);
18
+ const doc = readFileSync(
19
+ repoPath('docs/profile-migration.md'),
20
+ 'utf8'
21
+ ).replaceAll('\r\n', '\n');
22
+ assert.equal(
23
+ doc
24
+ .slice(
25
+ doc.indexOf(BEGIN_MARKER) + BEGIN_MARKER.length,
26
+ doc.indexOf(END_MARKER)
27
+ )
28
+ .trim(),
29
+ table
30
+ );
31
+ });