resend-cli 1.2.2 → 1.3.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.
- package/README.md +25 -10
- package/dist/cli.cjs +539 -0
- package/package.json +31 -12
- package/.claude/settings.local.json +0 -5
- package/.github/scripts/pr-title-check.js +0 -34
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/pr-title-check.yml +0 -13
- package/.github/workflows/release.yml +0 -120
- package/.github/workflows/test-install-windows.yml +0 -48
- package/CHANGELOG.md +0 -31
- package/biome.json +0 -36
- package/bun.lock +0 -73
- package/bunfig.toml +0 -2
- package/docs/agent-dx-gaps.md +0 -167
- package/docs/missing-commands.md +0 -58
- package/docs/production-readiness.md +0 -99
- package/docs/secure-key-storage.md +0 -174
- package/install.ps1 +0 -141
- package/install.sh +0 -301
- package/renovate.json +0 -4
- package/src/cli.ts +0 -92
- package/src/commands/api-keys/create.ts +0 -114
- package/src/commands/api-keys/delete.ts +0 -47
- package/src/commands/api-keys/index.ts +0 -26
- package/src/commands/api-keys/list.ts +0 -35
- package/src/commands/api-keys/utils.ts +0 -8
- package/src/commands/auth/index.ts +0 -20
- package/src/commands/auth/login.ts +0 -234
- package/src/commands/auth/logout.ts +0 -105
- package/src/commands/broadcasts/create.ts +0 -196
- package/src/commands/broadcasts/delete.ts +0 -46
- package/src/commands/broadcasts/get.ts +0 -59
- package/src/commands/broadcasts/index.ts +0 -43
- package/src/commands/broadcasts/list.ts +0 -60
- package/src/commands/broadcasts/send.ts +0 -56
- package/src/commands/broadcasts/update.ts +0 -95
- package/src/commands/broadcasts/utils.ts +0 -35
- package/src/commands/contact-properties/create.ts +0 -118
- package/src/commands/contact-properties/delete.ts +0 -48
- package/src/commands/contact-properties/get.ts +0 -46
- package/src/commands/contact-properties/index.ts +0 -48
- package/src/commands/contact-properties/list.ts +0 -68
- package/src/commands/contact-properties/update.ts +0 -88
- package/src/commands/contact-properties/utils.ts +0 -17
- package/src/commands/contacts/add-segment.ts +0 -78
- package/src/commands/contacts/create.ts +0 -122
- package/src/commands/contacts/delete.ts +0 -49
- package/src/commands/contacts/get.ts +0 -53
- package/src/commands/contacts/index.ts +0 -58
- package/src/commands/contacts/list.ts +0 -57
- package/src/commands/contacts/remove-segment.ts +0 -48
- package/src/commands/contacts/segments.ts +0 -39
- package/src/commands/contacts/topics.ts +0 -45
- package/src/commands/contacts/update-topics.ts +0 -90
- package/src/commands/contacts/update.ts +0 -77
- package/src/commands/contacts/utils.ts +0 -119
- package/src/commands/doctor.ts +0 -216
- package/src/commands/domains/create.ts +0 -83
- package/src/commands/domains/delete.ts +0 -42
- package/src/commands/domains/get.ts +0 -47
- package/src/commands/domains/index.ts +0 -35
- package/src/commands/domains/list.ts +0 -53
- package/src/commands/domains/update.ts +0 -75
- package/src/commands/domains/utils.ts +0 -44
- package/src/commands/domains/verify.ts +0 -38
- package/src/commands/emails/batch.ts +0 -140
- package/src/commands/emails/index.ts +0 -24
- package/src/commands/emails/receiving/attachment.ts +0 -55
- package/src/commands/emails/receiving/attachments.ts +0 -68
- package/src/commands/emails/receiving/get.ts +0 -58
- package/src/commands/emails/receiving/index.ts +0 -28
- package/src/commands/emails/receiving/list.ts +0 -59
- package/src/commands/emails/receiving/utils.ts +0 -38
- package/src/commands/emails/send.ts +0 -189
- package/src/commands/open.ts +0 -24
- package/src/commands/segments/create.ts +0 -50
- package/src/commands/segments/delete.ts +0 -47
- package/src/commands/segments/get.ts +0 -38
- package/src/commands/segments/index.ts +0 -36
- package/src/commands/segments/list.ts +0 -58
- package/src/commands/segments/utils.ts +0 -7
- package/src/commands/teams/index.ts +0 -10
- package/src/commands/teams/list.ts +0 -35
- package/src/commands/teams/remove.ts +0 -86
- package/src/commands/teams/switch.ts +0 -76
- package/src/commands/topics/create.ts +0 -73
- package/src/commands/topics/delete.ts +0 -47
- package/src/commands/topics/get.ts +0 -42
- package/src/commands/topics/index.ts +0 -42
- package/src/commands/topics/list.ts +0 -34
- package/src/commands/topics/update.ts +0 -59
- package/src/commands/topics/utils.ts +0 -16
- package/src/commands/webhooks/create.ts +0 -128
- package/src/commands/webhooks/delete.ts +0 -49
- package/src/commands/webhooks/get.ts +0 -42
- package/src/commands/webhooks/index.ts +0 -44
- package/src/commands/webhooks/list.ts +0 -55
- package/src/commands/webhooks/update.ts +0 -83
- package/src/commands/webhooks/utils.ts +0 -36
- package/src/commands/whoami.ts +0 -71
- package/src/lib/actions.ts +0 -157
- package/src/lib/client.ts +0 -37
- package/src/lib/config.ts +0 -217
- package/src/lib/files.ts +0 -15
- package/src/lib/help-text.ts +0 -38
- package/src/lib/output.ts +0 -54
- package/src/lib/pagination.ts +0 -36
- package/src/lib/prompts.ts +0 -149
- package/src/lib/spinner.ts +0 -100
- package/src/lib/table.ts +0 -57
- package/src/lib/tty.ts +0 -28
- package/src/lib/update-check.ts +0 -172
- package/src/lib/version.ts +0 -4
- package/tests/commands/api-keys/create.test.ts +0 -195
- package/tests/commands/api-keys/delete.test.ts +0 -156
- package/tests/commands/api-keys/list.test.ts +0 -133
- package/tests/commands/auth/login.test.ts +0 -156
- package/tests/commands/auth/logout.test.ts +0 -146
- package/tests/commands/broadcasts/create.test.ts +0 -447
- package/tests/commands/broadcasts/delete.test.ts +0 -182
- package/tests/commands/broadcasts/get.test.ts +0 -146
- package/tests/commands/broadcasts/list.test.ts +0 -196
- package/tests/commands/broadcasts/send.test.ts +0 -161
- package/tests/commands/broadcasts/update.test.ts +0 -283
- package/tests/commands/contact-properties/create.test.ts +0 -250
- package/tests/commands/contact-properties/delete.test.ts +0 -183
- package/tests/commands/contact-properties/get.test.ts +0 -144
- package/tests/commands/contact-properties/list.test.ts +0 -180
- package/tests/commands/contact-properties/update.test.ts +0 -216
- package/tests/commands/contacts/add-segment.test.ts +0 -188
- package/tests/commands/contacts/create.test.ts +0 -270
- package/tests/commands/contacts/delete.test.ts +0 -192
- package/tests/commands/contacts/get.test.ts +0 -148
- package/tests/commands/contacts/list.test.ts +0 -175
- package/tests/commands/contacts/remove-segment.test.ts +0 -166
- package/tests/commands/contacts/segments.test.ts +0 -167
- package/tests/commands/contacts/topics.test.ts +0 -163
- package/tests/commands/contacts/update-topics.test.ts +0 -247
- package/tests/commands/contacts/update.test.ts +0 -205
- package/tests/commands/doctor.test.ts +0 -165
- package/tests/commands/domains/create.test.ts +0 -192
- package/tests/commands/domains/delete.test.ts +0 -156
- package/tests/commands/domains/get.test.ts +0 -137
- package/tests/commands/domains/list.test.ts +0 -164
- package/tests/commands/domains/update.test.ts +0 -223
- package/tests/commands/domains/verify.test.ts +0 -117
- package/tests/commands/emails/batch.test.ts +0 -313
- package/tests/commands/emails/receiving/attachment.test.ts +0 -140
- package/tests/commands/emails/receiving/attachments.test.ts +0 -168
- package/tests/commands/emails/receiving/get.test.ts +0 -140
- package/tests/commands/emails/receiving/list.test.ts +0 -181
- package/tests/commands/emails/send.test.ts +0 -309
- package/tests/commands/segments/create.test.ts +0 -163
- package/tests/commands/segments/delete.test.ts +0 -182
- package/tests/commands/segments/get.test.ts +0 -137
- package/tests/commands/segments/list.test.ts +0 -173
- package/tests/commands/teams/list.test.ts +0 -63
- package/tests/commands/teams/remove.test.ts +0 -103
- package/tests/commands/teams/switch.test.ts +0 -96
- package/tests/commands/topics/create.test.ts +0 -191
- package/tests/commands/topics/delete.test.ts +0 -156
- package/tests/commands/topics/get.test.ts +0 -125
- package/tests/commands/topics/list.test.ts +0 -124
- package/tests/commands/topics/update.test.ts +0 -177
- package/tests/commands/webhooks/create.test.ts +0 -224
- package/tests/commands/webhooks/delete.test.ts +0 -156
- package/tests/commands/webhooks/get.test.ts +0 -125
- package/tests/commands/webhooks/list.test.ts +0 -177
- package/tests/commands/webhooks/update.test.ts +0 -206
- package/tests/commands/whoami.test.ts +0 -99
- package/tests/helpers.ts +0 -93
- package/tests/lib/client.test.ts +0 -71
- package/tests/lib/config.test.ts +0 -445
- package/tests/lib/files.test.ts +0 -65
- package/tests/lib/help-text.test.ts +0 -97
- package/tests/lib/output.test.ts +0 -127
- package/tests/lib/prompts.test.ts +0 -178
- package/tests/lib/spinner.test.ts +0 -146
- package/tests/lib/table.test.ts +0 -63
- package/tests/lib/tty.test.ts +0 -85
- package/tests/lib/update-check.test.ts +0 -169
- package/tsconfig.json +0 -14
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterEach,
|
|
3
|
-
beforeEach,
|
|
4
|
-
describe,
|
|
5
|
-
expect,
|
|
6
|
-
mock,
|
|
7
|
-
spyOn,
|
|
8
|
-
test,
|
|
9
|
-
} from 'bun:test';
|
|
10
|
-
import {
|
|
11
|
-
captureTestEnv,
|
|
12
|
-
expectExit1,
|
|
13
|
-
mockExitThrow,
|
|
14
|
-
mockSdkError,
|
|
15
|
-
setNonInteractive,
|
|
16
|
-
setupOutputSpies,
|
|
17
|
-
} from '../../helpers';
|
|
18
|
-
|
|
19
|
-
const mockCreate = mock(async () => ({
|
|
20
|
-
data: { id: 'test-key-id', token: 're_testtoken1234567890' },
|
|
21
|
-
error: null,
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
mock.module('resend', () => ({
|
|
25
|
-
Resend: class MockResend {
|
|
26
|
-
constructor(public key: string) {}
|
|
27
|
-
apiKeys = { create: mockCreate };
|
|
28
|
-
},
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
describe('api-keys create command', () => {
|
|
32
|
-
const restoreEnv = captureTestEnv();
|
|
33
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
34
|
-
let errorSpy: ReturnType<typeof spyOn> | undefined;
|
|
35
|
-
let stderrSpy: ReturnType<typeof spyOn> | undefined;
|
|
36
|
-
let exitSpy: ReturnType<typeof spyOn> | undefined;
|
|
37
|
-
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
process.env.RESEND_API_KEY = 're_test_key';
|
|
40
|
-
mockCreate.mockClear();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
restoreEnv();
|
|
45
|
-
spies?.restore();
|
|
46
|
-
errorSpy?.mockRestore();
|
|
47
|
-
stderrSpy?.mockRestore();
|
|
48
|
-
exitSpy?.mockRestore();
|
|
49
|
-
spies = undefined;
|
|
50
|
-
errorSpy = undefined;
|
|
51
|
-
stderrSpy = undefined;
|
|
52
|
-
exitSpy = undefined;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('creates API key with --name flag', async () => {
|
|
56
|
-
spies = setupOutputSpies();
|
|
57
|
-
|
|
58
|
-
const { createApiKeyCommand } = await import(
|
|
59
|
-
'../../../src/commands/api-keys/create'
|
|
60
|
-
);
|
|
61
|
-
await createApiKeyCommand.parseAsync(['--name', 'Production'], {
|
|
62
|
-
from: 'user',
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
66
|
-
const args = mockCreate.mock.calls[0][0] as Record<string, unknown>;
|
|
67
|
-
expect(args.name).toBe('Production');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('passes permission flag to SDK', async () => {
|
|
71
|
-
spies = setupOutputSpies();
|
|
72
|
-
|
|
73
|
-
const { createApiKeyCommand } = await import(
|
|
74
|
-
'../../../src/commands/api-keys/create'
|
|
75
|
-
);
|
|
76
|
-
await createApiKeyCommand.parseAsync(
|
|
77
|
-
['--name', 'CI Token', '--permission', 'sending_access'],
|
|
78
|
-
{ from: 'user' },
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const args = mockCreate.mock.calls[0][0] as Record<string, unknown>;
|
|
82
|
-
expect(args.permission).toBe('sending_access');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('passes domain_id (snake_case) to SDK when --domain-id is provided', async () => {
|
|
86
|
-
spies = setupOutputSpies();
|
|
87
|
-
|
|
88
|
-
const { createApiKeyCommand } = await import(
|
|
89
|
-
'../../../src/commands/api-keys/create'
|
|
90
|
-
);
|
|
91
|
-
await createApiKeyCommand.parseAsync(
|
|
92
|
-
[
|
|
93
|
-
'--name',
|
|
94
|
-
'Domain Token',
|
|
95
|
-
'--permission',
|
|
96
|
-
'sending_access',
|
|
97
|
-
'--domain-id',
|
|
98
|
-
'domain-123',
|
|
99
|
-
],
|
|
100
|
-
{ from: 'user' },
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const args = mockCreate.mock.calls[0][0] as Record<string, unknown>;
|
|
104
|
-
expect(args.domain_id).toBe('domain-123');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('outputs JSON result when non-interactive', async () => {
|
|
108
|
-
spies = setupOutputSpies();
|
|
109
|
-
|
|
110
|
-
const { createApiKeyCommand } = await import(
|
|
111
|
-
'../../../src/commands/api-keys/create'
|
|
112
|
-
);
|
|
113
|
-
await createApiKeyCommand.parseAsync(['--name', 'Production'], {
|
|
114
|
-
from: 'user',
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const output = spies.logSpy.mock.calls[0][0] as string;
|
|
118
|
-
const parsed = JSON.parse(output);
|
|
119
|
-
expect(parsed.id).toBe('test-key-id');
|
|
120
|
-
expect(parsed.token).toBe('re_testtoken1234567890');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('errors with missing_name when --name absent in non-interactive mode', async () => {
|
|
124
|
-
setNonInteractive();
|
|
125
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
126
|
-
exitSpy = mockExitThrow();
|
|
127
|
-
|
|
128
|
-
const { createApiKeyCommand } = await import(
|
|
129
|
-
'../../../src/commands/api-keys/create'
|
|
130
|
-
);
|
|
131
|
-
await expectExit1(() =>
|
|
132
|
-
createApiKeyCommand.parseAsync([], { from: 'user' }),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
136
|
-
expect(output).toContain('missing_name');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('does not call SDK when missing_name error is raised', async () => {
|
|
140
|
-
setNonInteractive();
|
|
141
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
142
|
-
exitSpy = mockExitThrow();
|
|
143
|
-
|
|
144
|
-
const { createApiKeyCommand } = await import(
|
|
145
|
-
'../../../src/commands/api-keys/create'
|
|
146
|
-
);
|
|
147
|
-
await expectExit1(() =>
|
|
148
|
-
createApiKeyCommand.parseAsync([], { from: 'user' }),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
expect(mockCreate).not.toHaveBeenCalled();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('errors with auth_error when no API key', async () => {
|
|
155
|
-
setNonInteractive();
|
|
156
|
-
delete process.env.RESEND_API_KEY;
|
|
157
|
-
process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
|
|
158
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
159
|
-
exitSpy = mockExitThrow();
|
|
160
|
-
|
|
161
|
-
const { createApiKeyCommand } = await import(
|
|
162
|
-
'../../../src/commands/api-keys/create'
|
|
163
|
-
);
|
|
164
|
-
await expectExit1(() =>
|
|
165
|
-
createApiKeyCommand.parseAsync(['--name', 'Production'], {
|
|
166
|
-
from: 'user',
|
|
167
|
-
}),
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
171
|
-
expect(output).toContain('auth_error');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('errors with create_error when SDK returns an error', async () => {
|
|
175
|
-
setNonInteractive();
|
|
176
|
-
mockCreate.mockResolvedValueOnce(
|
|
177
|
-
mockSdkError('Name already taken', 'validation_error'),
|
|
178
|
-
);
|
|
179
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
180
|
-
stderrSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
181
|
-
exitSpy = mockExitThrow();
|
|
182
|
-
|
|
183
|
-
const { createApiKeyCommand } = await import(
|
|
184
|
-
'../../../src/commands/api-keys/create'
|
|
185
|
-
);
|
|
186
|
-
await expectExit1(() =>
|
|
187
|
-
createApiKeyCommand.parseAsync(['--name', 'Production'], {
|
|
188
|
-
from: 'user',
|
|
189
|
-
}),
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
193
|
-
expect(output).toContain('create_error');
|
|
194
|
-
});
|
|
195
|
-
});
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterEach,
|
|
3
|
-
beforeEach,
|
|
4
|
-
describe,
|
|
5
|
-
expect,
|
|
6
|
-
mock,
|
|
7
|
-
spyOn,
|
|
8
|
-
test,
|
|
9
|
-
} from 'bun:test';
|
|
10
|
-
import {
|
|
11
|
-
captureTestEnv,
|
|
12
|
-
expectExit1,
|
|
13
|
-
mockExitThrow,
|
|
14
|
-
mockSdkError,
|
|
15
|
-
setNonInteractive,
|
|
16
|
-
setupOutputSpies,
|
|
17
|
-
} from '../../helpers';
|
|
18
|
-
|
|
19
|
-
const mockRemove = mock(async () => ({
|
|
20
|
-
data: {},
|
|
21
|
-
error: null,
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
mock.module('resend', () => ({
|
|
25
|
-
Resend: class MockResend {
|
|
26
|
-
constructor(public key: string) {}
|
|
27
|
-
apiKeys = { remove: mockRemove };
|
|
28
|
-
},
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
describe('api-keys delete command', () => {
|
|
32
|
-
const restoreEnv = captureTestEnv();
|
|
33
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
34
|
-
let errorSpy: ReturnType<typeof spyOn> | undefined;
|
|
35
|
-
let stderrSpy: ReturnType<typeof spyOn> | undefined;
|
|
36
|
-
let exitSpy: ReturnType<typeof spyOn> | undefined;
|
|
37
|
-
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
process.env.RESEND_API_KEY = 're_test_key';
|
|
40
|
-
mockRemove.mockClear();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
restoreEnv();
|
|
45
|
-
spies?.restore();
|
|
46
|
-
errorSpy?.mockRestore();
|
|
47
|
-
stderrSpy?.mockRestore();
|
|
48
|
-
exitSpy?.mockRestore();
|
|
49
|
-
spies = undefined;
|
|
50
|
-
errorSpy = undefined;
|
|
51
|
-
stderrSpy = undefined;
|
|
52
|
-
exitSpy = undefined;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('deletes API key with --yes flag', async () => {
|
|
56
|
-
spies = setupOutputSpies();
|
|
57
|
-
|
|
58
|
-
const { deleteApiKeyCommand } = await import(
|
|
59
|
-
'../../../src/commands/api-keys/delete'
|
|
60
|
-
);
|
|
61
|
-
await deleteApiKeyCommand.parseAsync(['test-key-id', '--yes'], {
|
|
62
|
-
from: 'user',
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
expect(mockRemove).toHaveBeenCalledWith('test-key-id');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('outputs synthesized deleted JSON on success', async () => {
|
|
69
|
-
spies = setupOutputSpies();
|
|
70
|
-
|
|
71
|
-
const { deleteApiKeyCommand } = await import(
|
|
72
|
-
'../../../src/commands/api-keys/delete'
|
|
73
|
-
);
|
|
74
|
-
await deleteApiKeyCommand.parseAsync(['test-key-id', '--yes'], {
|
|
75
|
-
from: 'user',
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const output = spies.logSpy.mock.calls[0][0] as string;
|
|
79
|
-
const parsed = JSON.parse(output);
|
|
80
|
-
expect(parsed.deleted).toBe(true);
|
|
81
|
-
expect(parsed.id).toBe('test-key-id');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('errors with confirmation_required when --yes absent in non-interactive mode', async () => {
|
|
85
|
-
setNonInteractive();
|
|
86
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
87
|
-
exitSpy = mockExitThrow();
|
|
88
|
-
|
|
89
|
-
const { deleteApiKeyCommand } = await import(
|
|
90
|
-
'../../../src/commands/api-keys/delete'
|
|
91
|
-
);
|
|
92
|
-
await expectExit1(() =>
|
|
93
|
-
deleteApiKeyCommand.parseAsync(['test-key-id'], { from: 'user' }),
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
97
|
-
expect(output).toContain('confirmation_required');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('does not call SDK when confirmation is required but not given', async () => {
|
|
101
|
-
setNonInteractive();
|
|
102
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
103
|
-
exitSpy = mockExitThrow();
|
|
104
|
-
|
|
105
|
-
const { deleteApiKeyCommand } = await import(
|
|
106
|
-
'../../../src/commands/api-keys/delete'
|
|
107
|
-
);
|
|
108
|
-
await expectExit1(() =>
|
|
109
|
-
deleteApiKeyCommand.parseAsync(['test-key-id'], { from: 'user' }),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
expect(mockRemove).not.toHaveBeenCalled();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('errors with auth_error when no API key', async () => {
|
|
116
|
-
setNonInteractive();
|
|
117
|
-
delete process.env.RESEND_API_KEY;
|
|
118
|
-
process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
|
|
119
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
120
|
-
exitSpy = mockExitThrow();
|
|
121
|
-
|
|
122
|
-
const { deleteApiKeyCommand } = await import(
|
|
123
|
-
'../../../src/commands/api-keys/delete'
|
|
124
|
-
);
|
|
125
|
-
await expectExit1(() =>
|
|
126
|
-
deleteApiKeyCommand.parseAsync(['test-key-id', '--yes'], {
|
|
127
|
-
from: 'user',
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
132
|
-
expect(output).toContain('auth_error');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('errors with delete_error when SDK returns an error', async () => {
|
|
136
|
-
setNonInteractive();
|
|
137
|
-
mockRemove.mockResolvedValueOnce(
|
|
138
|
-
mockSdkError('API key not found', 'not_found'),
|
|
139
|
-
);
|
|
140
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
141
|
-
stderrSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
142
|
-
exitSpy = mockExitThrow();
|
|
143
|
-
|
|
144
|
-
const { deleteApiKeyCommand } = await import(
|
|
145
|
-
'../../../src/commands/api-keys/delete'
|
|
146
|
-
);
|
|
147
|
-
await expectExit1(() =>
|
|
148
|
-
deleteApiKeyCommand.parseAsync(['test-key-id', '--yes'], {
|
|
149
|
-
from: 'user',
|
|
150
|
-
}),
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
154
|
-
expect(output).toContain('delete_error');
|
|
155
|
-
});
|
|
156
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterEach,
|
|
3
|
-
beforeEach,
|
|
4
|
-
describe,
|
|
5
|
-
expect,
|
|
6
|
-
mock,
|
|
7
|
-
spyOn,
|
|
8
|
-
test,
|
|
9
|
-
} from 'bun:test';
|
|
10
|
-
import {
|
|
11
|
-
captureTestEnv,
|
|
12
|
-
expectExit1,
|
|
13
|
-
mockExitThrow,
|
|
14
|
-
mockSdkError,
|
|
15
|
-
setNonInteractive,
|
|
16
|
-
setupOutputSpies,
|
|
17
|
-
} from '../../helpers';
|
|
18
|
-
|
|
19
|
-
const mockList = mock(async () => ({
|
|
20
|
-
data: {
|
|
21
|
-
object: 'list',
|
|
22
|
-
data: [
|
|
23
|
-
{
|
|
24
|
-
id: 'key-id-1',
|
|
25
|
-
name: 'Production Key',
|
|
26
|
-
created_at: '2026-01-01T00:00:00.000Z',
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
id: 'key-id-2',
|
|
30
|
-
name: 'Staging Key',
|
|
31
|
-
created_at: '2026-01-02T00:00:00.000Z',
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
|
-
error: null,
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
mock.module('resend', () => ({
|
|
39
|
-
Resend: class MockResend {
|
|
40
|
-
constructor(public key: string) {}
|
|
41
|
-
apiKeys = { list: mockList };
|
|
42
|
-
},
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
describe('api-keys list command', () => {
|
|
46
|
-
const restoreEnv = captureTestEnv();
|
|
47
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
48
|
-
let errorSpy: ReturnType<typeof spyOn> | undefined;
|
|
49
|
-
let stderrSpy: ReturnType<typeof spyOn> | undefined;
|
|
50
|
-
let exitSpy: ReturnType<typeof spyOn> | undefined;
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
process.env.RESEND_API_KEY = 're_test_key';
|
|
54
|
-
mockList.mockClear();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
afterEach(() => {
|
|
58
|
-
restoreEnv();
|
|
59
|
-
spies?.restore();
|
|
60
|
-
errorSpy?.mockRestore();
|
|
61
|
-
stderrSpy?.mockRestore();
|
|
62
|
-
exitSpy?.mockRestore();
|
|
63
|
-
spies = undefined;
|
|
64
|
-
errorSpy = undefined;
|
|
65
|
-
stderrSpy = undefined;
|
|
66
|
-
exitSpy = undefined;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('calls SDK list with no arguments', async () => {
|
|
70
|
-
spies = setupOutputSpies();
|
|
71
|
-
|
|
72
|
-
const { listApiKeysCommand } = await import(
|
|
73
|
-
'../../../src/commands/api-keys/list'
|
|
74
|
-
);
|
|
75
|
-
await listApiKeysCommand.parseAsync([], { from: 'user' });
|
|
76
|
-
|
|
77
|
-
expect(mockList).toHaveBeenCalledTimes(1);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('outputs JSON list when non-interactive', async () => {
|
|
81
|
-
spies = setupOutputSpies();
|
|
82
|
-
|
|
83
|
-
const { listApiKeysCommand } = await import(
|
|
84
|
-
'../../../src/commands/api-keys/list'
|
|
85
|
-
);
|
|
86
|
-
await listApiKeysCommand.parseAsync([], { from: 'user' });
|
|
87
|
-
|
|
88
|
-
const output = spies.logSpy.mock.calls[0][0] as string;
|
|
89
|
-
const parsed = JSON.parse(output);
|
|
90
|
-
expect(parsed.object).toBe('list');
|
|
91
|
-
expect(parsed.data).toHaveLength(2);
|
|
92
|
-
expect(parsed.data[0].id).toBe('key-id-1');
|
|
93
|
-
expect(parsed.data[0].name).toBe('Production Key');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('errors with auth_error when no API key', async () => {
|
|
97
|
-
setNonInteractive();
|
|
98
|
-
delete process.env.RESEND_API_KEY;
|
|
99
|
-
process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
|
|
100
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
101
|
-
exitSpy = mockExitThrow();
|
|
102
|
-
|
|
103
|
-
const { listApiKeysCommand } = await import(
|
|
104
|
-
'../../../src/commands/api-keys/list'
|
|
105
|
-
);
|
|
106
|
-
await expectExit1(() =>
|
|
107
|
-
listApiKeysCommand.parseAsync([], { from: 'user' }),
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
111
|
-
expect(output).toContain('auth_error');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('errors with list_error when SDK returns an error', async () => {
|
|
115
|
-
setNonInteractive();
|
|
116
|
-
mockList.mockResolvedValueOnce(
|
|
117
|
-
mockSdkError('Unauthorized', 'unauthorized'),
|
|
118
|
-
);
|
|
119
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
120
|
-
stderrSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
121
|
-
exitSpy = mockExitThrow();
|
|
122
|
-
|
|
123
|
-
const { listApiKeysCommand } = await import(
|
|
124
|
-
'../../../src/commands/api-keys/list'
|
|
125
|
-
);
|
|
126
|
-
await expectExit1(() =>
|
|
127
|
-
listApiKeysCommand.parseAsync([], { from: 'user' }),
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
131
|
-
expect(output).toContain('list_error');
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterEach,
|
|
3
|
-
beforeEach,
|
|
4
|
-
describe,
|
|
5
|
-
expect,
|
|
6
|
-
mock,
|
|
7
|
-
spyOn,
|
|
8
|
-
test,
|
|
9
|
-
} from 'bun:test';
|
|
10
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import { tmpdir } from 'node:os';
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
import {
|
|
14
|
-
captureTestEnv,
|
|
15
|
-
expectExit1,
|
|
16
|
-
mockExitThrow,
|
|
17
|
-
setupOutputSpies,
|
|
18
|
-
} from '../../helpers';
|
|
19
|
-
|
|
20
|
-
// Mock the Resend SDK
|
|
21
|
-
mock.module('resend', () => ({
|
|
22
|
-
Resend: class MockResend {
|
|
23
|
-
constructor(public key: string) {}
|
|
24
|
-
domains = {
|
|
25
|
-
list: mock(async () => ({ data: { data: [] }, error: null })),
|
|
26
|
-
};
|
|
27
|
-
},
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
describe('login command', () => {
|
|
31
|
-
const restoreEnv = captureTestEnv();
|
|
32
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
33
|
-
let errorSpy: ReturnType<typeof spyOn> | undefined;
|
|
34
|
-
let exitSpy: ReturnType<typeof spyOn> | undefined;
|
|
35
|
-
let tmpDir: string;
|
|
36
|
-
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
tmpDir = join(
|
|
39
|
-
tmpdir(),
|
|
40
|
-
`resend-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
41
|
-
);
|
|
42
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
43
|
-
process.env.XDG_CONFIG_HOME = tmpDir;
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
afterEach(() => {
|
|
47
|
-
restoreEnv();
|
|
48
|
-
spies?.restore();
|
|
49
|
-
spies = undefined;
|
|
50
|
-
errorSpy?.mockRestore();
|
|
51
|
-
errorSpy = undefined;
|
|
52
|
-
exitSpy?.mockRestore();
|
|
53
|
-
exitSpy = undefined;
|
|
54
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('rejects key not starting with re_', async () => {
|
|
58
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
59
|
-
exitSpy = mockExitThrow();
|
|
60
|
-
|
|
61
|
-
const { loginCommand } = await import('../../../src/commands/auth/login');
|
|
62
|
-
await expectExit1(() =>
|
|
63
|
-
loginCommand.parseAsync(['--key', 'bad_key'], { from: 'user' }),
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('stores valid key to credentials.json', async () => {
|
|
68
|
-
spies = setupOutputSpies();
|
|
69
|
-
|
|
70
|
-
const { loginCommand } = await import('../../../src/commands/auth/login');
|
|
71
|
-
await loginCommand.parseAsync(['--key', 're_valid_test_key_123'], {
|
|
72
|
-
from: 'user',
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
76
|
-
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
77
|
-
expect(data.teams.default.api_key).toBe('re_valid_test_key_123');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('requires --key in non-interactive mode', async () => {
|
|
81
|
-
spies = setupOutputSpies();
|
|
82
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
83
|
-
exitSpy = mockExitThrow();
|
|
84
|
-
|
|
85
|
-
const { loginCommand } = await import('../../../src/commands/auth/login');
|
|
86
|
-
await expectExit1(() => loginCommand.parseAsync([], { from: 'user' }));
|
|
87
|
-
|
|
88
|
-
expect(errorSpy).toBeDefined();
|
|
89
|
-
const output = errorSpy?.mock.calls[0][0] as string;
|
|
90
|
-
expect(output).toContain('missing_key');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('non-interactive login stores as default when teams exist', async () => {
|
|
94
|
-
// Pre-populate credentials with an existing team
|
|
95
|
-
const configDir = join(tmpDir, 'resend');
|
|
96
|
-
mkdirSync(configDir, { recursive: true });
|
|
97
|
-
writeFileSync(
|
|
98
|
-
join(configDir, 'credentials.json'),
|
|
99
|
-
JSON.stringify({
|
|
100
|
-
active_team: 'production',
|
|
101
|
-
teams: { production: { api_key: 're_old_key_1234' } },
|
|
102
|
-
}),
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
spies = setupOutputSpies();
|
|
106
|
-
|
|
107
|
-
const { loginCommand } = await import('../../../src/commands/auth/login');
|
|
108
|
-
await loginCommand.parseAsync(['--key', 're_new_key_5678'], {
|
|
109
|
-
from: 'user',
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
113
|
-
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
114
|
-
// Non-interactive without --team flag stores as 'default' (no picker)
|
|
115
|
-
expect(data.teams.default.api_key).toBe('re_new_key_5678');
|
|
116
|
-
// Original team should still exist
|
|
117
|
-
expect(data.teams.production.api_key).toBe('re_old_key_1234');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('auto-switches to team specified via --team flag', async () => {
|
|
121
|
-
spies = setupOutputSpies();
|
|
122
|
-
|
|
123
|
-
const { Command } = await import('@commander-js/extra-typings');
|
|
124
|
-
const { loginCommand } = await import('../../../src/commands/auth/login');
|
|
125
|
-
const program = new Command()
|
|
126
|
-
.option('--team <name>')
|
|
127
|
-
.option('--json')
|
|
128
|
-
.option('--api-key <key>')
|
|
129
|
-
.option('-q, --quiet')
|
|
130
|
-
.addCommand(loginCommand);
|
|
131
|
-
|
|
132
|
-
// First store a default key
|
|
133
|
-
const configDir = join(tmpDir, 'resend');
|
|
134
|
-
mkdirSync(configDir, { recursive: true });
|
|
135
|
-
writeFileSync(
|
|
136
|
-
join(configDir, 'credentials.json'),
|
|
137
|
-
JSON.stringify({
|
|
138
|
-
active_team: 'default',
|
|
139
|
-
teams: { default: { api_key: 're_old_key_1234' } },
|
|
140
|
-
}),
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
await program.parseAsync(
|
|
144
|
-
['login', '--key', 're_staging_key_123', '--team', 'staging'],
|
|
145
|
-
{ from: 'user' },
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// @ts-expect-error — reset parent to avoid polluting the shared singleton
|
|
149
|
-
loginCommand.parent = null;
|
|
150
|
-
|
|
151
|
-
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
152
|
-
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
153
|
-
expect(data.active_team).toBe('staging');
|
|
154
|
-
expect(data.teams.staging.api_key).toBe('re_staging_key_123');
|
|
155
|
-
});
|
|
156
|
-
});
|