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,137 +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 mockGet = mock(async () => ({
|
|
20
|
-
data: {
|
|
21
|
-
object: 'segment' as const,
|
|
22
|
-
id: '3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c',
|
|
23
|
-
name: 'Newsletter Subscribers',
|
|
24
|
-
created_at: '2026-01-01T00:00:00.000Z',
|
|
25
|
-
},
|
|
26
|
-
error: null,
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
mock.module('resend', () => ({
|
|
30
|
-
Resend: class MockResend {
|
|
31
|
-
constructor(public key: string) {}
|
|
32
|
-
segments = { get: mockGet };
|
|
33
|
-
},
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
describe('segments get command', () => {
|
|
37
|
-
const restoreEnv = captureTestEnv();
|
|
38
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
39
|
-
let errorSpy: ReturnType<typeof spyOn> | undefined;
|
|
40
|
-
let stderrSpy: ReturnType<typeof spyOn> | undefined;
|
|
41
|
-
let exitSpy: ReturnType<typeof spyOn> | undefined;
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
process.env.RESEND_API_KEY = 're_test_key';
|
|
45
|
-
mockGet.mockClear();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
afterEach(() => {
|
|
49
|
-
restoreEnv();
|
|
50
|
-
spies?.restore();
|
|
51
|
-
errorSpy?.mockRestore();
|
|
52
|
-
stderrSpy?.mockRestore();
|
|
53
|
-
exitSpy?.mockRestore();
|
|
54
|
-
spies = undefined;
|
|
55
|
-
errorSpy = undefined;
|
|
56
|
-
stderrSpy = undefined;
|
|
57
|
-
exitSpy = undefined;
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('calls SDK with the provided segment ID', async () => {
|
|
61
|
-
spies = setupOutputSpies();
|
|
62
|
-
|
|
63
|
-
const { getSegmentCommand } = await import(
|
|
64
|
-
'../../../src/commands/segments/get'
|
|
65
|
-
);
|
|
66
|
-
await getSegmentCommand.parseAsync(
|
|
67
|
-
['3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c'],
|
|
68
|
-
{ from: 'user' },
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
72
|
-
expect(mockGet.mock.calls[0][0]).toBe(
|
|
73
|
-
'3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c',
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('outputs JSON segment data when non-interactive', async () => {
|
|
78
|
-
spies = setupOutputSpies();
|
|
79
|
-
|
|
80
|
-
const { getSegmentCommand } = await import(
|
|
81
|
-
'../../../src/commands/segments/get'
|
|
82
|
-
);
|
|
83
|
-
await getSegmentCommand.parseAsync(
|
|
84
|
-
['3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c'],
|
|
85
|
-
{ from: 'user' },
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const output = spies.logSpy.mock.calls[0][0] as string;
|
|
89
|
-
const parsed = JSON.parse(output);
|
|
90
|
-
expect(parsed.object).toBe('segment');
|
|
91
|
-
expect(parsed.id).toBe('3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c');
|
|
92
|
-
expect(parsed.name).toBe('Newsletter Subscribers');
|
|
93
|
-
expect(parsed.created_at).toBe('2026-01-01T00:00:00.000Z');
|
|
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 { getSegmentCommand } = await import(
|
|
104
|
-
'../../../src/commands/segments/get'
|
|
105
|
-
);
|
|
106
|
-
await expectExit1(() =>
|
|
107
|
-
getSegmentCommand.parseAsync(['3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c'], {
|
|
108
|
-
from: 'user',
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
113
|
-
expect(output).toContain('auth_error');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('errors with fetch_error when SDK returns an error', async () => {
|
|
117
|
-
setNonInteractive();
|
|
118
|
-
mockGet.mockResolvedValueOnce(
|
|
119
|
-
mockSdkError('Segment not found', 'not_found'),
|
|
120
|
-
);
|
|
121
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
122
|
-
stderrSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
123
|
-
exitSpy = mockExitThrow();
|
|
124
|
-
|
|
125
|
-
const { getSegmentCommand } = await import(
|
|
126
|
-
'../../../src/commands/segments/get'
|
|
127
|
-
);
|
|
128
|
-
await expectExit1(() =>
|
|
129
|
-
getSegmentCommand.parseAsync(['00000000-0000-0000-0000-000000000000'], {
|
|
130
|
-
from: 'user',
|
|
131
|
-
}),
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
135
|
-
expect(output).toContain('fetch_error');
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,173 +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' as const,
|
|
22
|
-
data: [
|
|
23
|
-
{
|
|
24
|
-
id: '3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c',
|
|
25
|
-
name: 'Newsletter Subscribers',
|
|
26
|
-
created_at: '2026-01-01T00:00:00.000Z',
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
has_more: false,
|
|
30
|
-
},
|
|
31
|
-
error: null,
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
mock.module('resend', () => ({
|
|
35
|
-
Resend: class MockResend {
|
|
36
|
-
constructor(public key: string) {}
|
|
37
|
-
segments = { list: mockList };
|
|
38
|
-
},
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
describe('segments list command', () => {
|
|
42
|
-
const restoreEnv = captureTestEnv();
|
|
43
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
44
|
-
let errorSpy: ReturnType<typeof spyOn> | undefined;
|
|
45
|
-
let stderrSpy: ReturnType<typeof spyOn> | undefined;
|
|
46
|
-
let exitSpy: ReturnType<typeof spyOn> | undefined;
|
|
47
|
-
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
process.env.RESEND_API_KEY = 're_test_key';
|
|
50
|
-
mockList.mockClear();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
afterEach(() => {
|
|
54
|
-
restoreEnv();
|
|
55
|
-
spies?.restore();
|
|
56
|
-
errorSpy?.mockRestore();
|
|
57
|
-
stderrSpy?.mockRestore();
|
|
58
|
-
exitSpy?.mockRestore();
|
|
59
|
-
spies = undefined;
|
|
60
|
-
errorSpy = undefined;
|
|
61
|
-
stderrSpy = undefined;
|
|
62
|
-
exitSpy = undefined;
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('calls SDK with default limit of 10', async () => {
|
|
66
|
-
spies = setupOutputSpies();
|
|
67
|
-
|
|
68
|
-
const { listSegmentsCommand } = await import(
|
|
69
|
-
'../../../src/commands/segments/list'
|
|
70
|
-
);
|
|
71
|
-
await listSegmentsCommand.parseAsync([], { from: 'user' });
|
|
72
|
-
|
|
73
|
-
expect(mockList).toHaveBeenCalledTimes(1);
|
|
74
|
-
const args = mockList.mock.calls[0][0] as Record<string, unknown>;
|
|
75
|
-
expect(args.limit).toBe(10);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('calls SDK with custom --limit', async () => {
|
|
79
|
-
spies = setupOutputSpies();
|
|
80
|
-
|
|
81
|
-
const { listSegmentsCommand } = await import(
|
|
82
|
-
'../../../src/commands/segments/list'
|
|
83
|
-
);
|
|
84
|
-
await listSegmentsCommand.parseAsync(['--limit', '25'], { from: 'user' });
|
|
85
|
-
|
|
86
|
-
const args = mockList.mock.calls[0][0] as Record<string, unknown>;
|
|
87
|
-
expect(args.limit).toBe(25);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('calls SDK with --after cursor', async () => {
|
|
91
|
-
spies = setupOutputSpies();
|
|
92
|
-
|
|
93
|
-
const { listSegmentsCommand } = await import(
|
|
94
|
-
'../../../src/commands/segments/list'
|
|
95
|
-
);
|
|
96
|
-
await listSegmentsCommand.parseAsync(['--after', 'cursor_xyz'], {
|
|
97
|
-
from: 'user',
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const args = mockList.mock.calls[0][0] as Record<string, unknown>;
|
|
101
|
-
expect(args.after).toBe('cursor_xyz');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('outputs JSON list when non-interactive', async () => {
|
|
105
|
-
spies = setupOutputSpies();
|
|
106
|
-
|
|
107
|
-
const { listSegmentsCommand } = await import(
|
|
108
|
-
'../../../src/commands/segments/list'
|
|
109
|
-
);
|
|
110
|
-
await listSegmentsCommand.parseAsync([], { from: 'user' });
|
|
111
|
-
|
|
112
|
-
const output = spies.logSpy.mock.calls[0][0] as string;
|
|
113
|
-
const parsed = JSON.parse(output);
|
|
114
|
-
expect(parsed.object).toBe('list');
|
|
115
|
-
expect(Array.isArray(parsed.data)).toBe(true);
|
|
116
|
-
expect(parsed.data[0].name).toBe('Newsletter Subscribers');
|
|
117
|
-
expect(parsed.has_more).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('errors with invalid_limit when --limit is out of range', async () => {
|
|
121
|
-
setNonInteractive();
|
|
122
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
123
|
-
exitSpy = mockExitThrow();
|
|
124
|
-
|
|
125
|
-
const { listSegmentsCommand } = await import(
|
|
126
|
-
'../../../src/commands/segments/list'
|
|
127
|
-
);
|
|
128
|
-
await expectExit1(() =>
|
|
129
|
-
listSegmentsCommand.parseAsync(['--limit', '0'], { from: 'user' }),
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
133
|
-
expect(output).toContain('invalid_limit');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('errors with auth_error when no API key', async () => {
|
|
137
|
-
setNonInteractive();
|
|
138
|
-
delete process.env.RESEND_API_KEY;
|
|
139
|
-
process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
|
|
140
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
141
|
-
exitSpy = mockExitThrow();
|
|
142
|
-
|
|
143
|
-
const { listSegmentsCommand } = await import(
|
|
144
|
-
'../../../src/commands/segments/list'
|
|
145
|
-
);
|
|
146
|
-
await expectExit1(() =>
|
|
147
|
-
listSegmentsCommand.parseAsync([], { from: 'user' }),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
151
|
-
expect(output).toContain('auth_error');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('errors with list_error when SDK returns an error', async () => {
|
|
155
|
-
setNonInteractive();
|
|
156
|
-
mockList.mockResolvedValueOnce(
|
|
157
|
-
mockSdkError('Server error', 'server_error'),
|
|
158
|
-
);
|
|
159
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
160
|
-
stderrSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
161
|
-
exitSpy = mockExitThrow();
|
|
162
|
-
|
|
163
|
-
const { listSegmentsCommand } = await import(
|
|
164
|
-
'../../../src/commands/segments/list'
|
|
165
|
-
);
|
|
166
|
-
await expectExit1(() =>
|
|
167
|
-
listSegmentsCommand.parseAsync([], { from: 'user' }),
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
171
|
-
expect(output).toContain('list_error');
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdirSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { Command } from '@commander-js/extra-typings';
|
|
6
|
-
import { storeApiKey } from '../../../src/lib/config';
|
|
7
|
-
import { captureTestEnv, setupOutputSpies } from '../../helpers';
|
|
8
|
-
|
|
9
|
-
async function createProgram() {
|
|
10
|
-
const { listCommand } = await import('../../../src/commands/teams/list');
|
|
11
|
-
return new Command()
|
|
12
|
-
.name('resend')
|
|
13
|
-
.option('--json', 'Force JSON output')
|
|
14
|
-
.option('--team <name>', 'Team profile')
|
|
15
|
-
.addCommand(listCommand);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('teams list command', () => {
|
|
19
|
-
const restoreEnv = captureTestEnv();
|
|
20
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
21
|
-
let tmpDir: string;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
tmpDir = join(
|
|
25
|
-
tmpdir(),
|
|
26
|
-
`resend-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
27
|
-
);
|
|
28
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
29
|
-
process.env.XDG_CONFIG_HOME = tmpDir;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
restoreEnv();
|
|
34
|
-
spies?.restore();
|
|
35
|
-
spies = undefined;
|
|
36
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('lists teams in JSON mode', async () => {
|
|
40
|
-
spies = setupOutputSpies();
|
|
41
|
-
storeApiKey('re_default', 'default');
|
|
42
|
-
storeApiKey('re_staging', 'staging');
|
|
43
|
-
|
|
44
|
-
const program = await createProgram();
|
|
45
|
-
await program.parseAsync(['list', '--json'], { from: 'user' });
|
|
46
|
-
|
|
47
|
-
const output = JSON.parse(spies.logSpy.mock.calls[0][0] as string);
|
|
48
|
-
expect(output.teams).toEqual([
|
|
49
|
-
{ name: 'default', active: true },
|
|
50
|
-
{ name: 'staging', active: false },
|
|
51
|
-
]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('shows message when no teams configured', async () => {
|
|
55
|
-
spies = setupOutputSpies();
|
|
56
|
-
|
|
57
|
-
const program = await createProgram();
|
|
58
|
-
await program.parseAsync(['list'], { from: 'user' });
|
|
59
|
-
|
|
60
|
-
const output = spies.logSpy.mock.calls[0][0] as string;
|
|
61
|
-
expect(output).toContain('No teams configured');
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
|
|
2
|
-
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { Command } from '@commander-js/extra-typings';
|
|
6
|
-
import { storeApiKey } from '../../../src/lib/config';
|
|
7
|
-
import {
|
|
8
|
-
captureTestEnv,
|
|
9
|
-
expectExit1,
|
|
10
|
-
mockExitThrow,
|
|
11
|
-
setupOutputSpies,
|
|
12
|
-
} from '../../helpers';
|
|
13
|
-
|
|
14
|
-
async function createProgram() {
|
|
15
|
-
const { removeCommand } = await import('../../../src/commands/teams/remove');
|
|
16
|
-
return new Command()
|
|
17
|
-
.name('resend')
|
|
18
|
-
.option('--json', 'Force JSON output')
|
|
19
|
-
.option('--team <name>', 'Team profile')
|
|
20
|
-
.addCommand(removeCommand);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('teams remove command', () => {
|
|
24
|
-
const restoreEnv = captureTestEnv();
|
|
25
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
26
|
-
let errorSpy: ReturnType<typeof spyOn> | undefined;
|
|
27
|
-
let exitSpy: ReturnType<typeof spyOn> | undefined;
|
|
28
|
-
let tmpDir: string;
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
tmpDir = join(
|
|
32
|
-
tmpdir(),
|
|
33
|
-
`resend-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
34
|
-
);
|
|
35
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
36
|
-
process.env.XDG_CONFIG_HOME = tmpDir;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
restoreEnv();
|
|
41
|
-
spies?.restore();
|
|
42
|
-
spies = undefined;
|
|
43
|
-
errorSpy?.mockRestore();
|
|
44
|
-
errorSpy = undefined;
|
|
45
|
-
exitSpy?.mockRestore();
|
|
46
|
-
exitSpy = undefined;
|
|
47
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('removes team in JSON mode', async () => {
|
|
51
|
-
spies = setupOutputSpies();
|
|
52
|
-
storeApiKey('re_default', 'default');
|
|
53
|
-
storeApiKey('re_staging', 'staging');
|
|
54
|
-
|
|
55
|
-
const program = await createProgram();
|
|
56
|
-
await program.parseAsync(['remove', 'staging', '--json'], { from: 'user' });
|
|
57
|
-
|
|
58
|
-
const output = JSON.parse(spies.logSpy.mock.calls[0][0] as string);
|
|
59
|
-
expect(output.success).toBe(true);
|
|
60
|
-
expect(output.removed_team).toBe('staging');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('deletes credentials file when last team removed', async () => {
|
|
64
|
-
spies = setupOutputSpies();
|
|
65
|
-
storeApiKey('re_only', 'only');
|
|
66
|
-
|
|
67
|
-
const program = await createProgram();
|
|
68
|
-
await program.parseAsync(['remove', 'only', '--json'], { from: 'user' });
|
|
69
|
-
|
|
70
|
-
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
71
|
-
expect(existsSync(configPath)).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('errors when name omitted in non-interactive mode', async () => {
|
|
75
|
-
spies = setupOutputSpies();
|
|
76
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
77
|
-
exitSpy = mockExitThrow();
|
|
78
|
-
storeApiKey('re_default');
|
|
79
|
-
|
|
80
|
-
const program = await createProgram();
|
|
81
|
-
await expectExit1(() =>
|
|
82
|
-
program.parseAsync(['remove', '--json'], { from: 'user' }),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const output = JSON.parse(errorSpy?.mock.calls[0][0] as string);
|
|
86
|
-
expect(output.error.code).toBe('missing_name');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('errors when team does not exist', async () => {
|
|
90
|
-
spies = setupOutputSpies();
|
|
91
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
92
|
-
exitSpy = mockExitThrow();
|
|
93
|
-
storeApiKey('re_default');
|
|
94
|
-
|
|
95
|
-
const program = await createProgram();
|
|
96
|
-
await expectExit1(() =>
|
|
97
|
-
program.parseAsync(['remove', 'nonexistent'], { from: 'user' }),
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const output = JSON.parse(errorSpy?.mock.calls[0][0] as string);
|
|
101
|
-
expect(output.error.code).toBe('remove_failed');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
|
|
2
|
-
import { mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { Command } from '@commander-js/extra-typings';
|
|
6
|
-
import { storeApiKey } from '../../../src/lib/config';
|
|
7
|
-
import {
|
|
8
|
-
captureTestEnv,
|
|
9
|
-
expectExit1,
|
|
10
|
-
mockExitThrow,
|
|
11
|
-
setupOutputSpies,
|
|
12
|
-
} from '../../helpers';
|
|
13
|
-
|
|
14
|
-
async function createProgram() {
|
|
15
|
-
const { switchCommand } = await import('../../../src/commands/teams/switch');
|
|
16
|
-
return new Command()
|
|
17
|
-
.name('resend')
|
|
18
|
-
.option('--json', 'Force JSON output')
|
|
19
|
-
.option('--team <name>', 'Team profile')
|
|
20
|
-
.addCommand(switchCommand);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('teams switch command', () => {
|
|
24
|
-
const restoreEnv = captureTestEnv();
|
|
25
|
-
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
26
|
-
let errorSpy: ReturnType<typeof spyOn> | undefined;
|
|
27
|
-
let exitSpy: ReturnType<typeof spyOn> | undefined;
|
|
28
|
-
let tmpDir: string;
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
tmpDir = join(
|
|
32
|
-
tmpdir(),
|
|
33
|
-
`resend-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
34
|
-
);
|
|
35
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
36
|
-
process.env.XDG_CONFIG_HOME = tmpDir;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
restoreEnv();
|
|
41
|
-
spies?.restore();
|
|
42
|
-
spies = undefined;
|
|
43
|
-
errorSpy?.mockRestore();
|
|
44
|
-
errorSpy = undefined;
|
|
45
|
-
exitSpy?.mockRestore();
|
|
46
|
-
exitSpy = undefined;
|
|
47
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('switches active team in JSON mode', async () => {
|
|
51
|
-
spies = setupOutputSpies();
|
|
52
|
-
storeApiKey('re_default', 'default');
|
|
53
|
-
storeApiKey('re_staging', 'staging');
|
|
54
|
-
|
|
55
|
-
const program = await createProgram();
|
|
56
|
-
await program.parseAsync(['switch', 'staging', '--json'], { from: 'user' });
|
|
57
|
-
|
|
58
|
-
const output = JSON.parse(spies.logSpy.mock.calls[0][0] as string);
|
|
59
|
-
expect(output.success).toBe(true);
|
|
60
|
-
expect(output.active_team).toBe('staging');
|
|
61
|
-
|
|
62
|
-
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
63
|
-
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
64
|
-
expect(data.active_team).toBe('staging');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('errors when name omitted in non-interactive mode', async () => {
|
|
68
|
-
spies = setupOutputSpies();
|
|
69
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
70
|
-
exitSpy = mockExitThrow();
|
|
71
|
-
storeApiKey('re_default');
|
|
72
|
-
|
|
73
|
-
const program = await createProgram();
|
|
74
|
-
await expectExit1(() =>
|
|
75
|
-
program.parseAsync(['switch', '--json'], { from: 'user' }),
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const output = JSON.parse(errorSpy?.mock.calls[0][0] as string);
|
|
79
|
-
expect(output.error.code).toBe('missing_name');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('errors when team does not exist', async () => {
|
|
83
|
-
spies = setupOutputSpies();
|
|
84
|
-
errorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
85
|
-
exitSpy = mockExitThrow();
|
|
86
|
-
storeApiKey('re_default');
|
|
87
|
-
|
|
88
|
-
const program = await createProgram();
|
|
89
|
-
await expectExit1(() =>
|
|
90
|
-
program.parseAsync(['switch', 'nonexistent'], { from: 'user' }),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const output = JSON.parse(errorSpy?.mock.calls[0][0] as string);
|
|
94
|
-
expect(output.error.code).toBe('switch_failed');
|
|
95
|
-
});
|
|
96
|
-
});
|