@fgv/ks 5.1.0-25
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/README.md +58 -0
- package/bin/ks.js +18 -0
- package/config/jest.config.json +12 -0
- package/config/rig.json +4 -0
- package/eslint.config.js +24 -0
- package/package.json +55 -0
- package/src/app.ts +595 -0
- package/src/cli.ts +1 -0
- package/src/help.ts +116 -0
- package/src/index.ts +21 -0
- package/src/io.ts +179 -0
- package/src/keystore.ts +196 -0
- package/src/template.ts +61 -0
- package/test/mocks/clipboardy.js +5 -0
- package/test/unit/app.test.ts +112 -0
- package/test/unit/help.test.ts +45 -0
- package/test/unit/io.test.ts +173 -0
- package/test/unit/keystore.test.ts +448 -0
- package/test/unit/template.test.ts +33 -0
- package/tsconfig.json +8 -0
- package/tsconfig.test.json +8 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import '@fgv/ts-utils-jest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
import { getHelpText } from '../../src/help';
|
|
5
|
+
|
|
6
|
+
describe('help text', () => {
|
|
7
|
+
test('getHelpText without a topic returns the overview', () => {
|
|
8
|
+
expect(getHelpText()).toSucceedAndSatisfy((helpText) => {
|
|
9
|
+
expect(helpText).toContain('ks manages ts-extras keystore files.');
|
|
10
|
+
expect(helpText).toContain('ks help template');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('getHelpText for template includes template guidance', () => {
|
|
15
|
+
expect(getHelpText('template')).toSucceedAndSatisfy((helpText) => {
|
|
16
|
+
expect(helpText).toContain('ks export template format');
|
|
17
|
+
expect(helpText).toContain('--template-string <text>');
|
|
18
|
+
expect(helpText).toContain('{{xai}}');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('getHelpText for password includes password sources', () => {
|
|
23
|
+
expect(getHelpText('password')).toSucceedAndSatisfy((helpText) => {
|
|
24
|
+
expect(helpText).toContain('FGV_KS_PASSWORD');
|
|
25
|
+
expect(helpText).toContain('KS_PASSWORD');
|
|
26
|
+
expect(helpText).toContain('ks session --var FGV_KS_PASSWORD');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('getHelpText rejects unknown topics', () => {
|
|
31
|
+
expect(getHelpText('missing')).toFailWith(
|
|
32
|
+
"Unknown help topic 'missing'. Available topics: overview, commands, password, template"
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('getHelpText can return command-specific help when a program is provided', () => {
|
|
37
|
+
const program = new Command().name('ks').description('Manage ts-extras keystore files');
|
|
38
|
+
program.command('put <name>').description('Store a secret in the keystore');
|
|
39
|
+
|
|
40
|
+
expect(getHelpText('put', program)).toSucceedAndSatisfy((helpText) => {
|
|
41
|
+
expect(helpText).toContain('Usage: ks put [options] <name>');
|
|
42
|
+
expect(helpText).toContain('Store a secret in the keystore');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
jest.mock('readline', () => ({
|
|
2
|
+
createInterface: jest.fn()
|
|
3
|
+
}));
|
|
4
|
+
|
|
5
|
+
import '@fgv/ts-utils-jest';
|
|
6
|
+
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import * as readline from 'readline';
|
|
10
|
+
|
|
11
|
+
import { defaultKeystorePath, promptHidden, promptVisible, resolvePath } from '../../src/io';
|
|
12
|
+
|
|
13
|
+
describe('io helpers', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('defaultKeystorePath uses the home keystore name', () => {
|
|
19
|
+
expect(defaultKeystorePath()).toBe(path.join(os.homedir(), '.fgv-ks'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('resolvePath expands tilde paths', () => {
|
|
23
|
+
const resolved = resolvePath('~/fgv-ks-test');
|
|
24
|
+
expect(resolved).not.toContain('~');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('promptHidden rejects when stdin is not a TTY', async () => {
|
|
28
|
+
const originalStdin = process.stdin;
|
|
29
|
+
Object.defineProperty(process, 'stdin', {
|
|
30
|
+
configurable: true,
|
|
31
|
+
value: { isTTY: false }
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await promptHidden('Password: ');
|
|
36
|
+
expect(result).toFailWith(/interactive prompt requires a TTY/i);
|
|
37
|
+
} finally {
|
|
38
|
+
Object.defineProperty(process, 'stdin', {
|
|
39
|
+
configurable: true,
|
|
40
|
+
value: originalStdin
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('promptHidden reads typed characters and resolves on Enter', async () => {
|
|
46
|
+
const originalStdin = process.stdin;
|
|
47
|
+
const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
48
|
+
const dataListeners: Array<(chunk: string) => void> = [];
|
|
49
|
+
const mockStdin = {
|
|
50
|
+
isTTY: true,
|
|
51
|
+
isRaw: false,
|
|
52
|
+
setRawMode: jest.fn(),
|
|
53
|
+
setEncoding: jest.fn(),
|
|
54
|
+
resume: jest.fn(),
|
|
55
|
+
pause: jest.fn(),
|
|
56
|
+
on: jest.fn((_event: string, listener: (chunk: string) => void) => {
|
|
57
|
+
dataListeners.push(listener);
|
|
58
|
+
}),
|
|
59
|
+
off: jest.fn()
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
Object.defineProperty(process, 'stdin', { configurable: true, value: mockStdin });
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const resultPromise = promptHidden('Password: ');
|
|
66
|
+
|
|
67
|
+
// Simulate typing 's', 'e', 'c', then Enter
|
|
68
|
+
for (const listener of dataListeners) {
|
|
69
|
+
listener('sec');
|
|
70
|
+
listener('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = await resultPromise;
|
|
74
|
+
expect(result).toSucceedWith('sec');
|
|
75
|
+
expect(stderrWriteSpy).toHaveBeenCalledWith('Password: ');
|
|
76
|
+
expect(stderrWriteSpy).toHaveBeenCalledWith('\n');
|
|
77
|
+
} finally {
|
|
78
|
+
Object.defineProperty(process, 'stdin', { configurable: true, value: originalStdin });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('promptHidden returns failure on Ctrl+C', async () => {
|
|
83
|
+
const originalStdin = process.stdin;
|
|
84
|
+
jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
85
|
+
const dataListeners: Array<(chunk: string) => void> = [];
|
|
86
|
+
const mockStdin = {
|
|
87
|
+
isTTY: true,
|
|
88
|
+
isRaw: false,
|
|
89
|
+
setRawMode: jest.fn(),
|
|
90
|
+
setEncoding: jest.fn(),
|
|
91
|
+
resume: jest.fn(),
|
|
92
|
+
pause: jest.fn(),
|
|
93
|
+
on: jest.fn((_event: string, listener: (chunk: string) => void) => {
|
|
94
|
+
dataListeners.push(listener);
|
|
95
|
+
}),
|
|
96
|
+
off: jest.fn()
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
Object.defineProperty(process, 'stdin', { configurable: true, value: mockStdin });
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const resultPromise = promptHidden('Password: ');
|
|
103
|
+
|
|
104
|
+
for (const listener of dataListeners) {
|
|
105
|
+
listener('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = await resultPromise;
|
|
109
|
+
expect(result).toFailWith(/prompt cancelled/i);
|
|
110
|
+
} finally {
|
|
111
|
+
Object.defineProperty(process, 'stdin', { configurable: true, value: originalStdin });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('promptVisible rejects when stdin is not a TTY', async () => {
|
|
116
|
+
const originalStdin = process.stdin;
|
|
117
|
+
Object.defineProperty(process, 'stdin', {
|
|
118
|
+
configurable: true,
|
|
119
|
+
value: { isTTY: false }
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await promptVisible('Secret name: ');
|
|
124
|
+
expect(result).toFailWith(/interactive prompt requires a TTY/i);
|
|
125
|
+
} finally {
|
|
126
|
+
Object.defineProperty(process, 'stdin', {
|
|
127
|
+
configurable: true,
|
|
128
|
+
value: originalStdin
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('promptVisible writes the prompt to stderr and resolves the entered value', async () => {
|
|
134
|
+
const originalStdin = process.stdin;
|
|
135
|
+
const question = jest.fn((query: string, callback: (answer: string) => void) => {
|
|
136
|
+
callback('api-key');
|
|
137
|
+
});
|
|
138
|
+
const close = jest.fn();
|
|
139
|
+
const on = jest.fn();
|
|
140
|
+
const createInterfaceMock = jest.mocked(readline.createInterface);
|
|
141
|
+
createInterfaceMock.mockReturnValue({
|
|
142
|
+
close,
|
|
143
|
+
on,
|
|
144
|
+
question
|
|
145
|
+
} as unknown as readline.Interface);
|
|
146
|
+
const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
147
|
+
|
|
148
|
+
Object.defineProperty(process, 'stdin', {
|
|
149
|
+
configurable: true,
|
|
150
|
+
value: { isTTY: true }
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const result = await promptVisible('Secret name: ');
|
|
155
|
+
expect(result).toSucceedAndSatisfy((value) => {
|
|
156
|
+
expect(value).toBe('api-key');
|
|
157
|
+
});
|
|
158
|
+
expect(createInterfaceMock).toHaveBeenCalledWith({
|
|
159
|
+
input: process.stdin,
|
|
160
|
+
output: process.stderr,
|
|
161
|
+
terminal: true
|
|
162
|
+
});
|
|
163
|
+
expect(question).toHaveBeenCalledWith('Secret name: ', expect.any(Function));
|
|
164
|
+
expect(close).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(stderrWriteSpy).toHaveBeenCalledWith('\n');
|
|
166
|
+
} finally {
|
|
167
|
+
Object.defineProperty(process, 'stdin', {
|
|
168
|
+
configurable: true,
|
|
169
|
+
value: originalStdin
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
jest.mock('../../src/io', () => ({
|
|
2
|
+
defaultKeystorePath: jest.fn(() => '/home/test/.fgv-ks'),
|
|
3
|
+
readTextFile: jest.fn(),
|
|
4
|
+
writeTextFile: jest.fn(),
|
|
5
|
+
resolvePath: jest.fn((p: string) => p)
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
jest.mock('@fgv/ts-extras', () => ({
|
|
9
|
+
CryptoUtils: {
|
|
10
|
+
nodeCryptoProvider: {},
|
|
11
|
+
KeyStore: {
|
|
12
|
+
KeyStore: {
|
|
13
|
+
create: jest.fn(),
|
|
14
|
+
open: jest.fn()
|
|
15
|
+
},
|
|
16
|
+
Converters: {
|
|
17
|
+
keystoreFile: {
|
|
18
|
+
convert: jest.fn()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
import '@fgv/ts-utils-jest';
|
|
26
|
+
|
|
27
|
+
import fs from 'fs';
|
|
28
|
+
import { fail, succeed } from '@fgv/ts-utils';
|
|
29
|
+
import { CryptoUtils } from '@fgv/ts-extras';
|
|
30
|
+
|
|
31
|
+
import { readTextFile, writeTextFile } from '../../src/io';
|
|
32
|
+
import {
|
|
33
|
+
resolveKeystorePath,
|
|
34
|
+
loadKeystoreFile,
|
|
35
|
+
saveKeystoreFile,
|
|
36
|
+
openKeystore,
|
|
37
|
+
createKeystore,
|
|
38
|
+
changeKeystorePassword,
|
|
39
|
+
storeSecret,
|
|
40
|
+
readSecret,
|
|
41
|
+
listSecrets,
|
|
42
|
+
removeSecret
|
|
43
|
+
} from '../../src/keystore';
|
|
44
|
+
|
|
45
|
+
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
|
46
|
+
const mockReadTextFile = readTextFile as unknown as jest.Mock;
|
|
47
|
+
const mockWriteTextFile = writeTextFile as unknown as jest.Mock;
|
|
48
|
+
const mockKeystoreFileConvert = CryptoUtils.KeyStore.Converters.keystoreFile.convert as unknown as jest.Mock;
|
|
49
|
+
const mockKeystoreCreate = CryptoUtils.KeyStore.KeyStore.create as unknown as jest.Mock;
|
|
50
|
+
const mockKeystoreOpen = CryptoUtils.KeyStore.KeyStore.open as unknown as jest.Mock;
|
|
51
|
+
|
|
52
|
+
const testKeystoreFile = { format: 'keystore-v1' } as unknown as CryptoUtils.KeyStore.IKeyStoreFile;
|
|
53
|
+
|
|
54
|
+
const mockKeystore = {
|
|
55
|
+
unlock: jest.fn(),
|
|
56
|
+
initialize: jest.fn(),
|
|
57
|
+
save: jest.fn(),
|
|
58
|
+
changePassword: jest.fn(),
|
|
59
|
+
importApiKey: jest.fn(),
|
|
60
|
+
getApiKey: jest.fn(),
|
|
61
|
+
listSecrets: jest.fn(),
|
|
62
|
+
removeSecret: jest.fn()
|
|
63
|
+
} as unknown as CryptoUtils.KeyStore.KeyStore;
|
|
64
|
+
|
|
65
|
+
function setupSuccessfulOpen(): void {
|
|
66
|
+
mockReadTextFile.mockReturnValue(succeed('{"format":"keystore-v1"}'));
|
|
67
|
+
mockKeystoreFileConvert.mockReturnValue(succeed(testKeystoreFile));
|
|
68
|
+
mockKeystoreOpen.mockReturnValue(succeed(mockKeystore));
|
|
69
|
+
(mockKeystore.unlock as jest.Mock).mockResolvedValue(succeed(mockKeystore));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('keystore module', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
jest.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('resolveKeystorePath', () => {
|
|
78
|
+
test('returns the resolved form of a supplied path', () => {
|
|
79
|
+
expect(resolveKeystorePath('/custom/path')).toBe('/custom/path');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('falls back to the default path when no path is supplied', () => {
|
|
83
|
+
expect(resolveKeystorePath()).toBe('/home/test/.fgv-ks');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('loadKeystoreFile', () => {
|
|
88
|
+
test('reads, parses, and validates the keystore file', async () => {
|
|
89
|
+
mockReadTextFile.mockReturnValue(succeed('{"format":"keystore-v1"}'));
|
|
90
|
+
mockKeystoreFileConvert.mockReturnValue(succeed(testKeystoreFile));
|
|
91
|
+
|
|
92
|
+
const result = await loadKeystoreFile('/test/keystore');
|
|
93
|
+
expect(result).toSucceedWith(testKeystoreFile);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('fails when readTextFile fails', async () => {
|
|
97
|
+
mockReadTextFile.mockReturnValue(fail('File not found'));
|
|
98
|
+
|
|
99
|
+
const result = await loadKeystoreFile('/test/keystore');
|
|
100
|
+
expect(result).toFailWith(/file not found/i);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('fails when the file contains invalid JSON', async () => {
|
|
104
|
+
mockReadTextFile.mockReturnValue(succeed('not-valid-json'));
|
|
105
|
+
|
|
106
|
+
const result = await loadKeystoreFile('/test/keystore');
|
|
107
|
+
expect(result).toFailWith(/invalid keystore file/i);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('fails when the converter rejects the file format', async () => {
|
|
111
|
+
mockReadTextFile.mockReturnValue(succeed('{}'));
|
|
112
|
+
mockKeystoreFileConvert.mockReturnValue(fail('Unrecognized format'));
|
|
113
|
+
|
|
114
|
+
const result = await loadKeystoreFile('/test/keystore');
|
|
115
|
+
expect(result).toFailWith(/invalid keystore file/i);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('saveKeystoreFile', () => {
|
|
120
|
+
test('serializes and writes the keystore file', async () => {
|
|
121
|
+
mockWriteTextFile.mockReturnValue(succeed('/test/keystore'));
|
|
122
|
+
|
|
123
|
+
const result = await saveKeystoreFile('/test/keystore', testKeystoreFile);
|
|
124
|
+
expect(result).toSucceedWith('/test/keystore');
|
|
125
|
+
expect(mockWriteTextFile).toHaveBeenCalledWith(
|
|
126
|
+
'/test/keystore',
|
|
127
|
+
expect.stringContaining('keystore-v1')
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('fails when writeTextFile fails', async () => {
|
|
132
|
+
mockWriteTextFile.mockReturnValue(fail('Permission denied'));
|
|
133
|
+
|
|
134
|
+
const result = await saveKeystoreFile('/test/keystore', testKeystoreFile);
|
|
135
|
+
expect(result).toFailWith(/permission denied/i);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('openKeystore', () => {
|
|
140
|
+
test('loads, opens, and unlocks the keystore', async () => {
|
|
141
|
+
setupSuccessfulOpen();
|
|
142
|
+
|
|
143
|
+
const result = await openKeystore('/test/keystore', 'password');
|
|
144
|
+
expect(result).toSucceedAndSatisfy((opened) => {
|
|
145
|
+
expect(opened.path).toBe('/test/keystore');
|
|
146
|
+
expect(opened.keystore).toBe(mockKeystore);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('fails when loading the file fails', async () => {
|
|
151
|
+
mockReadTextFile.mockReturnValue(fail('Not found'));
|
|
152
|
+
|
|
153
|
+
const result = await openKeystore('/test/keystore', 'password');
|
|
154
|
+
expect(result).toFailWith(/not found/i);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('fails when KeyStore.open fails', async () => {
|
|
158
|
+
mockReadTextFile.mockReturnValue(succeed('{}'));
|
|
159
|
+
mockKeystoreFileConvert.mockReturnValue(succeed(testKeystoreFile));
|
|
160
|
+
mockKeystoreOpen.mockReturnValue(fail('Invalid keystore format'));
|
|
161
|
+
|
|
162
|
+
const result = await openKeystore('/test/keystore', 'password');
|
|
163
|
+
expect(result).toFailWith(/invalid keystore format/i);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('fails when unlock fails', async () => {
|
|
167
|
+
mockReadTextFile.mockReturnValue(succeed('{}'));
|
|
168
|
+
mockKeystoreFileConvert.mockReturnValue(succeed(testKeystoreFile));
|
|
169
|
+
mockKeystoreOpen.mockReturnValue(succeed(mockKeystore));
|
|
170
|
+
(mockKeystore.unlock as jest.Mock).mockResolvedValue(fail('Wrong password'));
|
|
171
|
+
|
|
172
|
+
const result = await openKeystore('/test/keystore', 'password');
|
|
173
|
+
expect(result).toFailWith(/wrong password/i);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('createKeystore', () => {
|
|
178
|
+
test('creates, initializes, saves, and persists a new keystore', async () => {
|
|
179
|
+
mockExistsSync.mockReturnValue(false);
|
|
180
|
+
mockKeystoreCreate.mockReturnValue(succeed(mockKeystore));
|
|
181
|
+
(mockKeystore.initialize as jest.Mock).mockResolvedValue(succeed(mockKeystore));
|
|
182
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
183
|
+
mockWriteTextFile.mockReturnValue(succeed('/home/test/.fgv-ks'));
|
|
184
|
+
|
|
185
|
+
const result = await createKeystore(undefined, 'password');
|
|
186
|
+
expect(result).toSucceedAndSatisfy((opened) => {
|
|
187
|
+
expect(opened.keystore).toBe(mockKeystore);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('fails when a readable keystore file already exists at the path', async () => {
|
|
192
|
+
mockExistsSync.mockReturnValue(true);
|
|
193
|
+
mockReadTextFile.mockReturnValue(succeed('{"format":"keystore-v1"}'));
|
|
194
|
+
|
|
195
|
+
const result = await createKeystore('/test/keystore', 'password');
|
|
196
|
+
expect(result).toFailWith(/keystore already exists/i);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('fails when the file exists but cannot be read', async () => {
|
|
200
|
+
mockExistsSync.mockReturnValue(true);
|
|
201
|
+
mockReadTextFile.mockReturnValue(fail('Permission denied'));
|
|
202
|
+
|
|
203
|
+
const result = await createKeystore('/test/keystore', 'password');
|
|
204
|
+
expect(result).toFailWith(/exists but cannot be read/i);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('fails when KeyStore.create fails', async () => {
|
|
208
|
+
mockExistsSync.mockReturnValue(false);
|
|
209
|
+
mockKeystoreCreate.mockReturnValue(fail('Crypto unavailable'));
|
|
210
|
+
|
|
211
|
+
const result = await createKeystore('/test/keystore', 'password');
|
|
212
|
+
expect(result).toFailWith(/failed to create keystore/i);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('fails when initialization fails', async () => {
|
|
216
|
+
mockExistsSync.mockReturnValue(false);
|
|
217
|
+
mockKeystoreCreate.mockReturnValue(succeed(mockKeystore));
|
|
218
|
+
(mockKeystore.initialize as jest.Mock).mockResolvedValue(fail('Init error'));
|
|
219
|
+
|
|
220
|
+
const result = await createKeystore('/test/keystore', 'password');
|
|
221
|
+
expect(result).toFailWith(/failed to initialize keystore/i);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('fails when save fails', async () => {
|
|
225
|
+
mockExistsSync.mockReturnValue(false);
|
|
226
|
+
mockKeystoreCreate.mockReturnValue(succeed(mockKeystore));
|
|
227
|
+
(mockKeystore.initialize as jest.Mock).mockResolvedValue(succeed(mockKeystore));
|
|
228
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(fail('Save error'));
|
|
229
|
+
|
|
230
|
+
const result = await createKeystore('/test/keystore', 'password');
|
|
231
|
+
expect(result).toFailWith(/failed to save keystore/i);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('fails when writing the keystore file fails', async () => {
|
|
235
|
+
mockExistsSync.mockReturnValue(false);
|
|
236
|
+
mockKeystoreCreate.mockReturnValue(succeed(mockKeystore));
|
|
237
|
+
(mockKeystore.initialize as jest.Mock).mockResolvedValue(succeed(mockKeystore));
|
|
238
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
239
|
+
mockWriteTextFile.mockReturnValue(fail('Disk full'));
|
|
240
|
+
|
|
241
|
+
const result = await createKeystore('/test/keystore', 'password');
|
|
242
|
+
expect(result).toFailWith(/failed to write keystore file/i);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('changeKeystorePassword', () => {
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
setupSuccessfulOpen();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('opens, changes the password, saves, and persists', async () => {
|
|
252
|
+
(mockKeystore.changePassword as jest.Mock).mockResolvedValue(succeed(mockKeystore));
|
|
253
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
254
|
+
mockWriteTextFile.mockReturnValue(succeed('/test/keystore'));
|
|
255
|
+
|
|
256
|
+
const result = await changeKeystorePassword('/test/keystore', 'old-pw', 'new-pw');
|
|
257
|
+
expect(result).toSucceedAndSatisfy((opened) => {
|
|
258
|
+
expect(opened.keystore).toBe(mockKeystore);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('fails when openKeystore fails', async () => {
|
|
263
|
+
mockReadTextFile.mockReturnValue(fail('Not found'));
|
|
264
|
+
|
|
265
|
+
const result = await changeKeystorePassword('/test/keystore', 'old-pw', 'new-pw');
|
|
266
|
+
expect(result).toFailWith(/not found/i);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('fails when changePassword fails', async () => {
|
|
270
|
+
(mockKeystore.changePassword as jest.Mock).mockResolvedValue(fail('Wrong password'));
|
|
271
|
+
|
|
272
|
+
const result = await changeKeystorePassword('/test/keystore', 'old-pw', 'new-pw');
|
|
273
|
+
expect(result).toFailWith(/failed to change password/i);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('fails when save fails', async () => {
|
|
277
|
+
(mockKeystore.changePassword as jest.Mock).mockResolvedValue(succeed(mockKeystore));
|
|
278
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(fail('Save error'));
|
|
279
|
+
|
|
280
|
+
const result = await changeKeystorePassword('/test/keystore', 'old-pw', 'new-pw');
|
|
281
|
+
expect(result).toFailWith(/failed to save keystore/i);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('fails when writing the file fails', async () => {
|
|
285
|
+
(mockKeystore.changePassword as jest.Mock).mockResolvedValue(succeed(mockKeystore));
|
|
286
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
287
|
+
mockWriteTextFile.mockReturnValue(fail('Write failed'));
|
|
288
|
+
|
|
289
|
+
const result = await changeKeystorePassword('/test/keystore', 'old-pw', 'new-pw');
|
|
290
|
+
expect(result).toFailWith(/failed to write keystore file/i);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('storeSecret', () => {
|
|
295
|
+
beforeEach(() => {
|
|
296
|
+
setupSuccessfulOpen();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('opens, stores, saves, and persists a secret', async () => {
|
|
300
|
+
(mockKeystore.importApiKey as jest.Mock).mockResolvedValue(succeed({ entry: {}, replaced: false }));
|
|
301
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
302
|
+
mockWriteTextFile.mockReturnValue(succeed('/test/keystore'));
|
|
303
|
+
|
|
304
|
+
const result = await storeSecret('/test/keystore', 'password', 'my-key', 'secret-value');
|
|
305
|
+
expect(result).toSucceed();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('passes options through to importApiKey', async () => {
|
|
309
|
+
(mockKeystore.importApiKey as jest.Mock).mockResolvedValue(succeed({ entry: {}, replaced: true }));
|
|
310
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
311
|
+
mockWriteTextFile.mockReturnValue(succeed('/test/keystore'));
|
|
312
|
+
|
|
313
|
+
await storeSecret('/test/keystore', 'password', 'my-key', 'secret-value', {
|
|
314
|
+
description: 'A test key',
|
|
315
|
+
replace: true
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(mockKeystore.importApiKey).toHaveBeenCalledWith('my-key', 'secret-value', {
|
|
319
|
+
description: 'A test key',
|
|
320
|
+
replace: true
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('fails when openKeystore fails', async () => {
|
|
325
|
+
mockReadTextFile.mockReturnValue(fail('Not found'));
|
|
326
|
+
|
|
327
|
+
const result = await storeSecret('/test/keystore', 'password', 'my-key', 'secret-value');
|
|
328
|
+
expect(result).toFailWith(/not found/i);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('fails when importApiKey fails', async () => {
|
|
332
|
+
(mockKeystore.importApiKey as jest.Mock).mockResolvedValue(fail('Key already exists'));
|
|
333
|
+
|
|
334
|
+
const result = await storeSecret('/test/keystore', 'password', 'my-key', 'secret-value');
|
|
335
|
+
expect(result).toFailWith(/failed to store secret/i);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('fails when save fails', async () => {
|
|
339
|
+
(mockKeystore.importApiKey as jest.Mock).mockResolvedValue(succeed({ entry: {}, replaced: false }));
|
|
340
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(fail('Save error'));
|
|
341
|
+
|
|
342
|
+
const result = await storeSecret('/test/keystore', 'password', 'my-key', 'secret-value');
|
|
343
|
+
expect(result).toFailWith(/failed to save keystore/i);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('fails when writing the file fails', async () => {
|
|
347
|
+
(mockKeystore.importApiKey as jest.Mock).mockResolvedValue(succeed({ entry: {}, replaced: false }));
|
|
348
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
349
|
+
mockWriteTextFile.mockReturnValue(fail('Disk full'));
|
|
350
|
+
|
|
351
|
+
const result = await storeSecret('/test/keystore', 'password', 'my-key', 'secret-value');
|
|
352
|
+
expect(result).toFailWith(/failed to write keystore file/i);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('readSecret', () => {
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
setupSuccessfulOpen();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('opens and returns the secret value', async () => {
|
|
362
|
+
(mockKeystore.getApiKey as jest.Mock).mockReturnValue(succeed('secret-value'));
|
|
363
|
+
|
|
364
|
+
const result = await readSecret('/test/keystore', 'password', 'my-key');
|
|
365
|
+
expect(result).toSucceedWith('secret-value');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('fails when openKeystore fails', async () => {
|
|
369
|
+
mockReadTextFile.mockReturnValue(fail('Not found'));
|
|
370
|
+
|
|
371
|
+
const result = await readSecret('/test/keystore', 'password', 'my-key');
|
|
372
|
+
expect(result).toFailWith(/not found/i);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('fails when getApiKey fails', async () => {
|
|
376
|
+
(mockKeystore.getApiKey as jest.Mock).mockReturnValue(fail('Key not found'));
|
|
377
|
+
|
|
378
|
+
const result = await readSecret('/test/keystore', 'password', 'my-key');
|
|
379
|
+
expect(result).toFailWith(/key not found/i);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('listSecrets', () => {
|
|
384
|
+
beforeEach(() => {
|
|
385
|
+
setupSuccessfulOpen();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('opens and returns the list of secret names', async () => {
|
|
389
|
+
(mockKeystore.listSecrets as jest.Mock).mockReturnValue(succeed(['key1', 'key2']));
|
|
390
|
+
|
|
391
|
+
const result = await listSecrets('/test/keystore', 'password');
|
|
392
|
+
expect(result).toSucceedWith(['key1', 'key2']);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('fails when openKeystore fails', async () => {
|
|
396
|
+
mockReadTextFile.mockReturnValue(fail('Not found'));
|
|
397
|
+
|
|
398
|
+
const result = await listSecrets('/test/keystore', 'password');
|
|
399
|
+
expect(result).toFailWith(/not found/i);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('removeSecret', () => {
|
|
404
|
+
beforeEach(() => {
|
|
405
|
+
setupSuccessfulOpen();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('opens, removes, saves, and persists', async () => {
|
|
409
|
+
(mockKeystore.removeSecret as jest.Mock).mockResolvedValue(succeed({ entry: {} }));
|
|
410
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
411
|
+
mockWriteTextFile.mockReturnValue(succeed('/test/keystore'));
|
|
412
|
+
|
|
413
|
+
const result = await removeSecret('/test/keystore', 'password', 'my-key');
|
|
414
|
+
expect(result).toSucceed();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('fails when openKeystore fails', async () => {
|
|
418
|
+
mockReadTextFile.mockReturnValue(fail('Not found'));
|
|
419
|
+
|
|
420
|
+
const result = await removeSecret('/test/keystore', 'password', 'my-key');
|
|
421
|
+
expect(result).toFailWith(/not found/i);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('fails when removeSecret fails', async () => {
|
|
425
|
+
(mockKeystore.removeSecret as jest.Mock).mockResolvedValue(fail('Key not found'));
|
|
426
|
+
|
|
427
|
+
const result = await removeSecret('/test/keystore', 'password', 'my-key');
|
|
428
|
+
expect(result).toFailWith(/failed to remove secret/i);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('fails when save fails', async () => {
|
|
432
|
+
(mockKeystore.removeSecret as jest.Mock).mockResolvedValue(succeed({ entry: {} }));
|
|
433
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(fail('Save error'));
|
|
434
|
+
|
|
435
|
+
const result = await removeSecret('/test/keystore', 'password', 'my-key');
|
|
436
|
+
expect(result).toFailWith(/failed to save keystore/i);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('fails when writing the file fails', async () => {
|
|
440
|
+
(mockKeystore.removeSecret as jest.Mock).mockResolvedValue(succeed({ entry: {} }));
|
|
441
|
+
(mockKeystore.save as jest.Mock).mockResolvedValue(succeed(testKeystoreFile));
|
|
442
|
+
mockWriteTextFile.mockReturnValue(fail('Write failed'));
|
|
443
|
+
|
|
444
|
+
const result = await removeSecret('/test/keystore', 'password', 'my-key');
|
|
445
|
+
expect(result).toFailWith(/failed to write keystore file/i);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
});
|