browser-commander 0.24.0 → 0.26.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 (72) hide show
  1. package/.jscpd-baseline.json +1 -1
  2. package/CHANGELOG.md +32 -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 +610 -25
  12. package/src/browser/launcher.js +14 -0
  13. package/src/browser/migration/capabilities.json +117 -0
  14. package/src/browser/migration/chromium-history.sql +22 -0
  15. package/src/browser/migration/chromium-writers.js +85 -0
  16. package/src/browser/migration/cookies.js +4 -1
  17. package/src/browser/migration/data-classes.js +68 -0
  18. package/src/browser/migration/domains.js +32 -0
  19. package/src/browser/migration/firefox-history.js +106 -0
  20. package/src/browser/migration/firefox.js +39 -25
  21. package/src/browser/migration/history.js +161 -2
  22. package/src/browser/migration/index.js +229 -83
  23. package/src/browser/migration/os-crypt-keys.js +1 -5
  24. package/src/browser/migration/password-metadata.js +95 -0
  25. package/src/browser/migration/passwords.js +78 -41
  26. package/src/browser/migration/safari-import.js +100 -0
  27. package/src/browser/migration/safari.js +110 -0
  28. package/src/browser/migration/sqlite-snapshot.js +31 -65
  29. package/src/browser/migration/validation.js +48 -0
  30. package/src/browser/real-browser.js +18 -1
  31. package/src/browser/safari-access.js +45 -0
  32. package/src/browser/safari-cookies.js +39 -36
  33. package/src/browser/safari-launch.js +42 -0
  34. package/src/browser/safari-profiles.js +83 -0
  35. package/src/browser/safari-support.js +98 -0
  36. package/src/browser/storage-state.js +28 -8
  37. package/src/browser/system-browser.js +31 -143
  38. package/src/browser/webdriver.js +64 -19
  39. package/src/cli/args.js +2 -0
  40. package/src/cli/commands.js +2 -0
  41. package/src/cli/high-level-methods.js +2 -0
  42. package/src/cli/sessions.js +5 -0
  43. package/src/core/webdriver-page.js +62 -0
  44. package/src/exports.js +6 -0
  45. package/src/traces/recorder.js +5 -0
  46. package/tests/e2e/safari.e2e.test.js +104 -0
  47. package/tests/unit/browser/attach/snapshot.test.js +30 -0
  48. package/tests/unit/browser/browser-cookies.test.js +35 -0
  49. package/tests/unit/browser/catalogue-launch.test.js +93 -0
  50. package/tests/unit/browser/cookie-sources.test.js +92 -17
  51. package/tests/unit/browser/credential-catalogue.test.js +50 -0
  52. package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
  53. package/tests/unit/browser/migration/data-classes.test.js +89 -0
  54. package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
  55. package/tests/unit/browser/migration/firefox-history.test.js +130 -0
  56. package/tests/unit/browser/migration/history-metadata.test.js +74 -0
  57. package/tests/unit/browser/migration/index.test.js +16 -1
  58. package/tests/unit/browser/migration/opera-keys.test.js +22 -0
  59. package/tests/unit/browser/migration/safari-data.test.js +143 -0
  60. package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
  61. package/tests/unit/browser/migration/validation.test.js +63 -0
  62. package/tests/unit/browser/migration/yandex.test.js +84 -0
  63. package/tests/unit/browser/real-browser.test.js +19 -0
  64. package/tests/unit/browser/safari-cookies.test.js +26 -1
  65. package/tests/unit/browser/safari-profiles.test.js +111 -0
  66. package/tests/unit/browser/safari.test.js +192 -0
  67. package/tests/unit/browser/webdriver.test.js +48 -3
  68. package/tests/unit/cli/dispatcher.test.js +5 -1
  69. package/tests/unit/cli/main.test.js +5 -0
  70. package/tests/unit/scripts/browser-support.test.js +14 -7
  71. package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
  72. package/tests/unit/scripts/migration-support.test.js +31 -0
@@ -1,17 +1,78 @@
1
1
  // feature-parity: sources.cookie-listing@native-typed
2
2
  import assert from 'node:assert';
3
3
  import { describe, it } from 'node:test';
4
+ import { mkdir, readFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import BetterSqlite3 from 'better-sqlite3';
4
7
 
5
8
  import {
6
9
  listCookieSources,
7
10
  resolveImportSource,
8
11
  } from '../../../src/browser/browser-cookies.js';
12
+ import { migrateProfile } from '../../../src/browser/migration/index.js';
9
13
  import { writeFirefoxProfile } from '../../helpers/migration-fixtures.js';
10
14
  import { useTempDirectories } from '../../helpers/temp-directory.js';
11
15
 
12
16
  const makeDirectory = useTempDirectories();
13
17
 
14
18
  describe('listCookieSources', () => {
19
+ it('matches only whole hosts and subdomains in both SQLite families', async () => {
20
+ const hosts = [
21
+ '.github.com',
22
+ 'api.GITHUB.COM.',
23
+ 'notgithub.com',
24
+ '.github.com.attacker.test',
25
+ '.gitXhub.com',
26
+ ];
27
+ for (const browser of ['firefox', 'chrome']) {
28
+ const homeDir = await makeDirectory('bc-source-domains-');
29
+ let file;
30
+ if (browser === 'firefox') {
31
+ const profile = await writeFirefoxProfile(
32
+ homeDir,
33
+ hosts.map((host, index) => ({
34
+ host,
35
+ name: `${index}`,
36
+ value: 'secret',
37
+ }))
38
+ );
39
+ file = path.join(profile, 'cookies.sqlite');
40
+ } else {
41
+ const profile = path.join(homeDir, '.config/google-chrome/Default');
42
+ await mkdir(profile, { recursive: true });
43
+ file = path.join(profile, 'Cookies');
44
+ const database = new BetterSqlite3(file);
45
+ try {
46
+ database.exec('CREATE TABLE cookies (host_key TEXT, value TEXT)');
47
+ for (const host of hosts) {
48
+ database
49
+ .prepare('INSERT INTO cookies VALUES (?, ?)')
50
+ .run(host, 'secret');
51
+ }
52
+ } finally {
53
+ database.close();
54
+ }
55
+ }
56
+ const before = await readFile(file);
57
+ const sources = await listCookieSources({
58
+ domains: ['GITHUB.COM.', 'git_hub.com', '%github.com'],
59
+ platform: 'linux',
60
+ homeDir,
61
+ environment: {},
62
+ });
63
+ assert.equal(sources.length, 1);
64
+ assert.equal(sources[0].browser, browser);
65
+ assert.equal(sources[0].cookies, hosts.length);
66
+ assert.deepEqual(sources[0].byDomain, {
67
+ 'GITHUB.COM.': 2,
68
+ 'git_hub.com': 0,
69
+ '%github.com': 0,
70
+ });
71
+ assert.ok(!JSON.stringify(sources).includes('secret'));
72
+ assert.deepEqual(await readFile(file), before);
73
+ }
74
+ });
75
+
15
76
  it('reports cookie counts per profile without reading values', async () => {
16
77
  const temporaryDirectory = await makeDirectory('bc-src-');
17
78
  const profilePath = await writeFirefoxProfile(temporaryDirectory, [
@@ -92,23 +153,37 @@ describe('resolveImportSource', () => {
92
153
  });
93
154
  });
94
155
 
95
- it('falls back to the installed browser that holds the domains', async () => {
96
- const homeDir = await makeDirectory('bc-import-fallback-');
97
- await writeFirefoxProfile(homeDir, otherCookie);
98
- await writeFirefoxProfile(homeDir, githubCookie, {
99
- root: '.librewolf',
100
- name: 'default',
101
- });
102
-
103
- const source = await resolve(homeDir);
104
- assert.equal(source.browser, 'librewolf');
105
- assert.equal(source.profile, 'default');
106
- assert.equal(source.warning.reason, 'default-browser-fallback');
107
- assert.equal(source.warning.item, 'librewolf');
108
- assert.match(
109
- source.warning.detail,
110
- /default browser \(firefox\) holds no cookies for github\.com; imported from librewolf/
111
- );
156
+ it('falls back when the default holds unrelated or lookalike domains', async () => {
157
+ for (const host of ['.other.test', '.notgithub.com']) {
158
+ const homeDir = await makeDirectory('bc-import-fallback-');
159
+ await writeFirefoxProfile(homeDir, [{ ...otherCookie[0], host }]);
160
+ await writeFirefoxProfile(homeDir, githubCookie, {
161
+ root: '.librewolf',
162
+ name: 'default',
163
+ });
164
+ const source = await resolve(homeDir);
165
+ assert.equal(source.browser, 'librewolf');
166
+ assert.equal(source.profile, 'default');
167
+ assert.equal(source.warning.reason, 'default-browser-fallback');
168
+ assert.equal(source.warning.item, 'librewolf');
169
+ assert.match(
170
+ source.warning.detail,
171
+ /default browser \(firefox\) holds no cookies for github\.com; imported from librewolf/
172
+ );
173
+ const report = await migrateProfile({
174
+ from: { browser: 'auto' },
175
+ to: path.join(homeDir, 'target'),
176
+ include: ['cookies'],
177
+ domains: ['github.com'],
178
+ platform: 'linux',
179
+ homeDir,
180
+ environment: {},
181
+ runCommand: firefoxIsDefault,
182
+ });
183
+ assert.equal(report.source.browser, 'librewolf');
184
+ assert.equal(report.migrated.cookies, 1);
185
+ assert.equal(report.cookies[0].domain, '.github.com');
186
+ }
112
187
  });
113
188
 
114
189
  it('falls back when the system default cannot be determined', async () => {
@@ -0,0 +1,50 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { BROWSER_SOURCES } from '../../../src/browser/browser-sources.js';
4
+ import { readSafeStoragePassword } from '../../../src/browser/browser-cookie-credentials.js';
5
+
6
+ describe('catalogue credential identities and Keychain diagnostics', () => {
7
+ it('requests every declared browser service rather than a four-browser table', async () => {
8
+ for (const browser of BROWSER_SOURCES.filter(
9
+ (entry) => entry.safeStorage
10
+ )) {
11
+ for (const name of [browser.id, ...(browser.aliases ?? [])]) {
12
+ const calls = [];
13
+ const password = await readSafeStoragePassword({
14
+ browser: name,
15
+ platform: 'darwin',
16
+ environment: {},
17
+ runCredentialCommand: async (command, args) => {
18
+ calls.push([command, args]);
19
+ return 'synthetic-password';
20
+ },
21
+ });
22
+ assert.equal(password, 'synthetic-password');
23
+ assert.deepEqual(calls, [
24
+ [
25
+ 'security',
26
+ ['find-generic-password', '-w', '-s', browser.safeStorage.service],
27
+ ],
28
+ ]);
29
+ }
30
+ }
31
+ });
32
+
33
+ for (const outcome of ['denied', 'empty']) {
34
+ it(`identifies the item and retry path when Keychain returns ${outcome}`, async () => {
35
+ await assert.rejects(
36
+ readSafeStoragePassword({
37
+ browser: 'chrome',
38
+ platform: 'darwin',
39
+ runCredentialCommand: async () => {
40
+ if (outcome === 'denied') {
41
+ throw new Error('security exited with code 36');
42
+ }
43
+ return '';
44
+ },
45
+ }),
46
+ /Chrome Safe Storage.*Keychain.*allow.*retry.*refresh=true/s
47
+ );
48
+ });
49
+ }
50
+ });
@@ -0,0 +1,79 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { it } from 'node:test';
6
+
7
+ import { openSqliteDatabase } from '../../../src/browser/browser-cookie-database.js';
8
+ import { readBrowserCookiesWithDependencies } from '../../../src/browser/browser-cookies.js';
9
+ import { migrateProfile } from '../../../src/browser/migration/index.js';
10
+
11
+ const fixture = await readFile(
12
+ new URL(
13
+ '../../../../tests/fixtures/firefox-cookie-expiry.sql',
14
+ import.meta.url
15
+ ),
16
+ 'utf8'
17
+ );
18
+
19
+ for (const version of [0, 15, 16, 17]) {
20
+ for (const migration of [false, true]) {
21
+ it(`normalizes Firefox schema ${version} expiry through ${migration ? 'migration' : 'installed reading'}`, async () => {
22
+ const homeDir = await mkdtemp(path.join(os.tmpdir(), 'bc-ff-expiry-'));
23
+ try {
24
+ const root = path.join(homeDir, '.mozilla', 'firefox');
25
+ const source = path.join(root, 'expiry.default-release');
26
+ await mkdir(source, { recursive: true });
27
+ await writeFile(
28
+ path.join(root, 'profiles.ini'),
29
+ '[Profile0]\nName=default-release\nIsRelative=1\nPath=expiry.default-release\nDefault=1\n'
30
+ );
31
+ const file = path.join(source, 'cookies.sqlite');
32
+ const database = await openSqliteDatabase(file);
33
+ try {
34
+ database.exec(fixture);
35
+ database.exec(`PRAGMA user_version = ${version}`);
36
+ if (version >= 16) {
37
+ database.exec(
38
+ 'UPDATE moz_cookies SET expiry = expiry * 1000 + 999 WHERE expiry > 0'
39
+ );
40
+ }
41
+ } finally {
42
+ database.close();
43
+ }
44
+ const before = await readFile(file);
45
+ const options = { platform: 'linux', homeDir, environment: {} };
46
+ const cookies = migration
47
+ ? (
48
+ await migrateProfile({
49
+ from: { browser: 'firefox', userDataDir: source },
50
+ to: path.join(homeDir, 'target'),
51
+ include: ['cookies'],
52
+ domains: ['expiry.example'],
53
+ ...options,
54
+ })
55
+ ).cookies
56
+ : await readBrowserCookiesWithDependencies(
57
+ {
58
+ browser: 'firefox',
59
+ domainFilter: 'expiry.example',
60
+ cache: false,
61
+ },
62
+ options
63
+ );
64
+ assert.equal(cookies.length, 3);
65
+ const expiry = Object.fromEntries(
66
+ cookies.map((cookie) => [cookie.name, cookie.expires])
67
+ );
68
+ assert.deepEqual(expiry, {
69
+ persistent: 2_000_000_001,
70
+ 'session-negative': -1,
71
+ 'session-zero': -1,
72
+ });
73
+ assert.deepEqual(await readFile(file), before);
74
+ } finally {
75
+ await rm(homeDir, { recursive: true, force: true });
76
+ }
77
+ });
78
+ }
79
+ }
@@ -0,0 +1,89 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { it } from 'node:test';
5
+
6
+ import {
7
+ ALL_DATA_CLASSES,
8
+ migrateProfile,
9
+ } from '../../../../src/browser/migration/index.js';
10
+ import { repoPath } from '../../../helpers/repo.js';
11
+ import { useTempDirectories } from '../../../helpers/temp-directory.js';
12
+
13
+ const makeTempDir = useTempDirectories('bc-data-classes-');
14
+ const classes = JSON.parse(
15
+ await readFile(repoPath('tests/fixtures/migration-data-classes.json'), 'utf8')
16
+ );
17
+
18
+ it('requires an actual boolean for payment-card consent before target writes', async () => {
19
+ const target = path.join(await makeTempDir(), 'new-profile');
20
+ await assert.rejects(
21
+ migrateProfile({
22
+ from: { browser: 'chrome', userDataDir: await makeTempDir() },
23
+ to: target,
24
+ include: ['paymentCards'],
25
+ includePaymentCards: 'true',
26
+ }),
27
+ /must be a boolean/u
28
+ );
29
+ await assert.rejects(readFile(path.join(target, 'Web Data')), {
30
+ code: 'ENOENT',
31
+ });
32
+ });
33
+
34
+ it('reports consented payment cards as unsupported until a native writer exists', async () => {
35
+ const report = await migrateProfile({
36
+ from: { browser: 'chrome', userDataDir: await makeTempDir() },
37
+ to: path.join(await makeTempDir(), 'new-profile'),
38
+ include: ['paymentCards'],
39
+ includePaymentCards: true,
40
+ });
41
+ assert.equal(report.migrated.paymentCards, 0);
42
+ assert.equal(report.skipped[0].reason, 'data-class-not-supported');
43
+ });
44
+
45
+ for (const browser of ['chrome', 'firefox', 'safari']) {
46
+ it(`reports every selected additional data class for ${browser} without reading protected card data`, async () => {
47
+ const source = await makeTempDir();
48
+ const target = path.join(await makeTempDir(), 'new-profile');
49
+ const profile =
50
+ browser === 'chrome' ? path.join(source, 'Default') : source;
51
+ await mkdir(profile, { recursive: true });
52
+ const marker = Buffer.from(
53
+ 'protected card store must remain unread and unchanged'
54
+ );
55
+ await writeFile(path.join(profile, 'Web Data'), marker);
56
+ const report = await migrateProfile({
57
+ from: { browser, userDataDir: source },
58
+ to: target,
59
+ include: classes.slice(6),
60
+ platform: 'darwin',
61
+ });
62
+ assert.deepEqual(ALL_DATA_CLASSES, classes);
63
+ assert.deepEqual(Object.keys(report.migrated), classes);
64
+ assert.ok(Object.values(report.migrated).every((count) => count === 0));
65
+ for (const type of classes.slice(6)) {
66
+ assert.ok(
67
+ report.skipped.some((entry) => entry.type === type),
68
+ type
69
+ );
70
+ }
71
+ assert.equal(
72
+ report.skipped.find(({ type }) => type === 'paymentCards').reason,
73
+ 'payment-card-consent-required'
74
+ );
75
+ const passkeys = report.skipped.filter(({ type }) => type === 'passkeys');
76
+ assert.deepEqual(
77
+ passkeys.map(({ item }) => item),
78
+ ['iCloud Keychain', 'Google Password Manager', 'Windows Hello']
79
+ );
80
+ for (const entry of passkeys) {
81
+ assert.equal(entry.reason, 'passkey-not-exportable');
82
+ assert.match(entry.detail, /persistent profile/u);
83
+ }
84
+ await assert.rejects(readFile(path.join(target, 'Web Data')), {
85
+ code: 'ENOENT',
86
+ });
87
+ assert.deepEqual(await readFile(path.join(profile, 'Web Data')), marker);
88
+ });
89
+ }
@@ -0,0 +1,212 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import path from 'node:path';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { randomBytes } from 'node:crypto';
6
+ import Database from 'better-sqlite3';
7
+ import { useTempDirectories } from '../../../helpers/temp-directory.js';
8
+ import { migrateHistory } from '../../../../src/browser/migration/history.js';
9
+ import { migratePasswords } from '../../../../src/browser/migration/passwords.js';
10
+ import { encryptChromiumValue } from '../../../../src/browser/migration/chromium-crypto.js';
11
+ import {
12
+ decryptChromiumCookie,
13
+ deriveChromiumCookieKey,
14
+ } from '../../../../src/browser/browser-cookie-crypto.js';
15
+
16
+ const temp = useTempDirectories('bc-domain-isolation-');
17
+ describe('per-site domain isolation (#119)', () => {
18
+ for (const domains of [['github.com'], []]) {
19
+ it(`re-encrypts login notes and ${domains.length ? 'filters' : 'preserves'} domain metadata`, async () => {
20
+ const source = await temp();
21
+ const target = await temp();
22
+ const sourceKey = randomBytes(16);
23
+ const targetKey = randomBytes(16);
24
+ const sourcePath = path.join(source, 'Login Data');
25
+ const db = new Database(sourcePath);
26
+ db.exec(
27
+ await readFile(
28
+ new URL(
29
+ '../../../../../tests/fixtures/password-domain-isolation.sql',
30
+ import.meta.url
31
+ ),
32
+ 'utf8'
33
+ )
34
+ );
35
+ const encrypted = (plaintext) =>
36
+ encryptChromiumValue({
37
+ plaintext,
38
+ key: sourceKey,
39
+ platform: 'linux',
40
+ prefix: 'v11',
41
+ });
42
+ db.prepare('UPDATE logins SET password_value=? WHERE id IN (7,8)').run(
43
+ encrypted('password')
44
+ );
45
+ const sourceNote = encrypted('private note ☃');
46
+ db.prepare(
47
+ 'UPDATE password_notes SET value=? WHERE id IN (70,71,72)'
48
+ ).run(sourceNote);
49
+ db.close();
50
+ const original = await readFile(sourcePath);
51
+ const report = await migratePasswords({
52
+ sourceProfileDir: source,
53
+ targetProfileDir: target,
54
+ domains,
55
+ platform: 'linux',
56
+ resolveSourceKey: () => sourceKey,
57
+ targetKey,
58
+ });
59
+ const filtered = domains.length > 0;
60
+ assert.equal(report.migrated, filtered ? 1 : 2);
61
+ const copy = new Database(path.join(target, 'Login Data'));
62
+ try {
63
+ assert.deepEqual(
64
+ copy.prepare('SELECT parent_id FROM insecure_credentials').all(),
65
+ filtered ? [{ parent_id: 7 }] : [{ parent_id: 7 }, { parent_id: 8 }]
66
+ );
67
+ assert.deepEqual(
68
+ copy.prepare('SELECT origin_domain FROM stats').all(),
69
+ filtered
70
+ ? [{ origin_domain: 'https://github.com' }]
71
+ : [
72
+ { origin_domain: 'https://github.com' },
73
+ { origin_domain: 'https://notgithub.com' },
74
+ ]
75
+ );
76
+ const notes = copy
77
+ .prepare(
78
+ 'SELECT id,value,date_created,confidential FROM password_notes'
79
+ )
80
+ .all();
81
+ assert.equal(notes.length, filtered ? 1 : 2);
82
+ assert.equal(notes[0].id, 70);
83
+ assert.equal(notes[0].date_created, 123);
84
+ assert.equal(notes[0].confidential, 1);
85
+ for (const note of notes) {
86
+ assert.equal(
87
+ decryptChromiumCookie({
88
+ encryptedValue: note.value,
89
+ databaseVersion: 0,
90
+ platform: 'linux',
91
+ key: targetKey,
92
+ }),
93
+ 'private note ☃'
94
+ );
95
+ }
96
+ for (const table of [
97
+ 'sync_entities_metadata',
98
+ 'sync_model_metadata',
99
+ 'future_password_metadata',
100
+ ]) {
101
+ const preserved = table === 'future_password_metadata' && !filtered;
102
+ assert.equal(
103
+ copy.prepare(`SELECT count(*) AS n FROM ${table}`).get().n,
104
+ preserved ? 1 : 0,
105
+ table
106
+ );
107
+ assert.equal(
108
+ report.warnings.some((warning) => warning.item === table),
109
+ !preserved
110
+ );
111
+ }
112
+ assert.ok(
113
+ report.skipped.some(
114
+ (entry) =>
115
+ entry.item === 'password_notes/73' &&
116
+ entry.reason === 'app-bound-v20'
117
+ )
118
+ );
119
+ } finally {
120
+ copy.close();
121
+ }
122
+ const bytes = await readFile(path.join(target, 'Login Data'));
123
+ assert.equal(bytes.includes(Buffer.from('unrelated-sync-marker')), false);
124
+ assert.equal(
125
+ bytes.includes(Buffer.from('unrelated-future-marker')),
126
+ !filtered
127
+ );
128
+ assert.equal(bytes.includes(sourceNote), false);
129
+ assert.deepEqual(await readFile(sourcePath), original);
130
+ });
131
+ }
132
+
133
+ it('excludes unrelated history URLs, their visits, and Top Sites', async () => {
134
+ const source = await temp();
135
+ const target = await temp();
136
+ const db = new Database(path.join(source, 'History'));
137
+ db.exec(
138
+ await readFile(
139
+ new URL(
140
+ '../../../../../tests/fixtures/history-domain-isolation.sql',
141
+ import.meta.url
142
+ ),
143
+ 'utf8'
144
+ )
145
+ );
146
+ db.close();
147
+ await migrateHistory({
148
+ sourceProfileDir: source,
149
+ targetProfileDir: target,
150
+ domains: ['github.com'],
151
+ });
152
+ const copy = new Database(path.join(target, 'History'));
153
+ try {
154
+ assert.equal(copy.prepare('SELECT count(*) AS n FROM urls').get().n, 1);
155
+ assert.equal(copy.prepare('SELECT count(*) AS n FROM visits').get().n, 1);
156
+ for (const table of [
157
+ 'content_annotations',
158
+ 'context_annotations',
159
+ 'segment_usage',
160
+ 'downloads',
161
+ 'downloads_url_chains',
162
+ 'downloads_slices',
163
+ ]) {
164
+ assert.equal(
165
+ copy.prepare(`SELECT count(*) AS n FROM ${table}`).get().n,
166
+ 1,
167
+ table
168
+ );
169
+ }
170
+ } finally {
171
+ copy.close();
172
+ }
173
+ });
174
+
175
+ it('filters passwords before decryption and removes undecryptable source values', async () => {
176
+ const source = await temp();
177
+ const target = await temp();
178
+ const key = deriveChromiumCookieKey('source', 'linux');
179
+ const db = new Database(path.join(source, 'Login Data'));
180
+ db.exec('CREATE TABLE logins(origin_url TEXT, password_value BLOB)');
181
+ const insert = db.prepare('INSERT INTO logins VALUES (?,?)');
182
+ insert.run(
183
+ 'https://github.com',
184
+ encryptChromiumValue({
185
+ plaintext: 'secret',
186
+ key,
187
+ platform: 'linux',
188
+ prefix: 'v11',
189
+ })
190
+ );
191
+ insert.run('https://notgithub.com', Buffer.from('v20unreadable'));
192
+ insert.run('https://locked.github.com', Buffer.from('v20unreadable'));
193
+ db.close();
194
+ const report = await migratePasswords({
195
+ sourceProfileDir: source,
196
+ targetProfileDir: target,
197
+ domains: ['github.com'],
198
+ platform: 'linux',
199
+ resolveSourceKey: () => key,
200
+ targetKey: key,
201
+ });
202
+ assert.equal(report.migrated, 1);
203
+ const copy = new Database(path.join(target, 'Login Data'));
204
+ try {
205
+ assert.deepEqual(copy.prepare('SELECT origin_url FROM logins').all(), [
206
+ { origin_url: 'https://github.com' },
207
+ ]);
208
+ } finally {
209
+ copy.close();
210
+ }
211
+ });
212
+ });