resend-cli 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.github/scripts/pr-title-check.js +34 -0
  3. package/.github/workflows/ci.yml +32 -0
  4. package/.github/workflows/pr-title-check.yml +13 -0
  5. package/.github/workflows/release.yml +93 -0
  6. package/CHANGELOG.md +31 -0
  7. package/LICENSE +21 -21
  8. package/README.md +416 -19
  9. package/biome.json +36 -0
  10. package/bun.lock +76 -0
  11. package/bunfig.toml +2 -0
  12. package/install.ps1 +140 -0
  13. package/install.sh +294 -0
  14. package/package.json +43 -22
  15. package/src/cli.ts +65 -0
  16. package/src/commands/api-keys/create.ts +114 -0
  17. package/src/commands/api-keys/delete.ts +47 -0
  18. package/src/commands/api-keys/index.ts +26 -0
  19. package/src/commands/api-keys/list.ts +35 -0
  20. package/src/commands/api-keys/utils.ts +8 -0
  21. package/src/commands/auth/index.ts +20 -0
  22. package/src/commands/auth/login.ts +211 -0
  23. package/src/commands/auth/logout.ts +105 -0
  24. package/src/commands/broadcasts/create.ts +196 -0
  25. package/src/commands/broadcasts/delete.ts +46 -0
  26. package/src/commands/broadcasts/get.ts +59 -0
  27. package/src/commands/broadcasts/index.ts +43 -0
  28. package/src/commands/broadcasts/list.ts +60 -0
  29. package/src/commands/broadcasts/send.ts +56 -0
  30. package/src/commands/broadcasts/update.ts +95 -0
  31. package/src/commands/broadcasts/utils.ts +35 -0
  32. package/src/commands/contact-properties/create.ts +118 -0
  33. package/src/commands/contact-properties/delete.ts +48 -0
  34. package/src/commands/contact-properties/get.ts +46 -0
  35. package/src/commands/contact-properties/index.ts +48 -0
  36. package/src/commands/contact-properties/list.ts +68 -0
  37. package/src/commands/contact-properties/update.ts +88 -0
  38. package/src/commands/contact-properties/utils.ts +17 -0
  39. package/src/commands/contacts/add-segment.ts +78 -0
  40. package/src/commands/contacts/create.ts +122 -0
  41. package/src/commands/contacts/delete.ts +49 -0
  42. package/src/commands/contacts/get.ts +53 -0
  43. package/src/commands/contacts/index.ts +58 -0
  44. package/src/commands/contacts/list.ts +57 -0
  45. package/src/commands/contacts/remove-segment.ts +48 -0
  46. package/src/commands/contacts/segments.ts +39 -0
  47. package/src/commands/contacts/topics.ts +45 -0
  48. package/src/commands/contacts/update-topics.ts +90 -0
  49. package/src/commands/contacts/update.ts +77 -0
  50. package/src/commands/contacts/utils.ts +119 -0
  51. package/src/commands/doctor.ts +298 -0
  52. package/src/commands/domains/create.ts +83 -0
  53. package/src/commands/domains/delete.ts +42 -0
  54. package/src/commands/domains/get.ts +47 -0
  55. package/src/commands/domains/index.ts +35 -0
  56. package/src/commands/domains/list.ts +53 -0
  57. package/src/commands/domains/update.ts +75 -0
  58. package/src/commands/domains/utils.ts +44 -0
  59. package/src/commands/domains/verify.ts +38 -0
  60. package/src/commands/emails/batch.ts +140 -0
  61. package/src/commands/emails/index.ts +24 -0
  62. package/src/commands/emails/receiving/attachment.ts +55 -0
  63. package/src/commands/emails/receiving/attachments.ts +68 -0
  64. package/src/commands/emails/receiving/get.ts +58 -0
  65. package/src/commands/emails/receiving/index.ts +28 -0
  66. package/src/commands/emails/receiving/list.ts +59 -0
  67. package/src/commands/emails/receiving/utils.ts +38 -0
  68. package/src/commands/emails/send.ts +189 -0
  69. package/src/commands/segments/create.ts +50 -0
  70. package/src/commands/segments/delete.ts +47 -0
  71. package/src/commands/segments/get.ts +38 -0
  72. package/src/commands/segments/index.ts +36 -0
  73. package/src/commands/segments/list.ts +58 -0
  74. package/src/commands/segments/utils.ts +7 -0
  75. package/src/commands/teams/index.ts +10 -0
  76. package/src/commands/teams/list.ts +35 -0
  77. package/src/commands/teams/remove.ts +83 -0
  78. package/src/commands/teams/switch.ts +73 -0
  79. package/src/commands/topics/create.ts +73 -0
  80. package/src/commands/topics/delete.ts +47 -0
  81. package/src/commands/topics/get.ts +42 -0
  82. package/src/commands/topics/index.ts +42 -0
  83. package/src/commands/topics/list.ts +34 -0
  84. package/src/commands/topics/update.ts +59 -0
  85. package/src/commands/topics/utils.ts +16 -0
  86. package/src/commands/webhooks/create.ts +128 -0
  87. package/src/commands/webhooks/delete.ts +49 -0
  88. package/src/commands/webhooks/get.ts +42 -0
  89. package/src/commands/webhooks/index.ts +44 -0
  90. package/src/commands/webhooks/list.ts +55 -0
  91. package/src/commands/webhooks/update.ts +83 -0
  92. package/src/commands/webhooks/utils.ts +36 -0
  93. package/src/commands/whoami.ts +71 -0
  94. package/src/lib/actions.ts +157 -0
  95. package/src/lib/client.ts +29 -0
  96. package/src/lib/config.ts +211 -0
  97. package/src/lib/files.ts +15 -0
  98. package/src/lib/help-text.ts +36 -0
  99. package/src/lib/output.ts +54 -0
  100. package/src/lib/pagination.ts +36 -0
  101. package/src/lib/prompts.ts +149 -0
  102. package/src/lib/spinner.ts +89 -0
  103. package/src/lib/table.ts +57 -0
  104. package/src/lib/tty.ts +28 -0
  105. package/src/lib/version.ts +4 -0
  106. package/tests/commands/api-keys/create.test.ts +195 -0
  107. package/tests/commands/api-keys/delete.test.ts +156 -0
  108. package/tests/commands/api-keys/list.test.ts +133 -0
  109. package/tests/commands/auth/login.test.ts +119 -0
  110. package/tests/commands/auth/logout.test.ts +146 -0
  111. package/tests/commands/broadcasts/create.test.ts +447 -0
  112. package/tests/commands/broadcasts/delete.test.ts +182 -0
  113. package/tests/commands/broadcasts/get.test.ts +146 -0
  114. package/tests/commands/broadcasts/list.test.ts +196 -0
  115. package/tests/commands/broadcasts/send.test.ts +161 -0
  116. package/tests/commands/broadcasts/update.test.ts +283 -0
  117. package/tests/commands/contact-properties/create.test.ts +250 -0
  118. package/tests/commands/contact-properties/delete.test.ts +183 -0
  119. package/tests/commands/contact-properties/get.test.ts +144 -0
  120. package/tests/commands/contact-properties/list.test.ts +180 -0
  121. package/tests/commands/contact-properties/update.test.ts +216 -0
  122. package/tests/commands/contacts/add-segment.test.ts +188 -0
  123. package/tests/commands/contacts/create.test.ts +270 -0
  124. package/tests/commands/contacts/delete.test.ts +192 -0
  125. package/tests/commands/contacts/get.test.ts +148 -0
  126. package/tests/commands/contacts/list.test.ts +175 -0
  127. package/tests/commands/contacts/remove-segment.test.ts +166 -0
  128. package/tests/commands/contacts/segments.test.ts +167 -0
  129. package/tests/commands/contacts/topics.test.ts +163 -0
  130. package/tests/commands/contacts/update-topics.test.ts +247 -0
  131. package/tests/commands/contacts/update.test.ts +205 -0
  132. package/tests/commands/doctor.test.ts +165 -0
  133. package/tests/commands/domains/create.test.ts +192 -0
  134. package/tests/commands/domains/delete.test.ts +156 -0
  135. package/tests/commands/domains/get.test.ts +137 -0
  136. package/tests/commands/domains/list.test.ts +164 -0
  137. package/tests/commands/domains/update.test.ts +223 -0
  138. package/tests/commands/domains/verify.test.ts +117 -0
  139. package/tests/commands/emails/batch.test.ts +313 -0
  140. package/tests/commands/emails/receiving/attachment.test.ts +140 -0
  141. package/tests/commands/emails/receiving/attachments.test.ts +168 -0
  142. package/tests/commands/emails/receiving/get.test.ts +140 -0
  143. package/tests/commands/emails/receiving/list.test.ts +181 -0
  144. package/tests/commands/emails/send.test.ts +309 -0
  145. package/tests/commands/segments/create.test.ts +163 -0
  146. package/tests/commands/segments/delete.test.ts +182 -0
  147. package/tests/commands/segments/get.test.ts +137 -0
  148. package/tests/commands/segments/list.test.ts +173 -0
  149. package/tests/commands/teams/list.test.ts +63 -0
  150. package/tests/commands/teams/remove.test.ts +103 -0
  151. package/tests/commands/teams/switch.test.ts +96 -0
  152. package/tests/commands/topics/create.test.ts +191 -0
  153. package/tests/commands/topics/delete.test.ts +156 -0
  154. package/tests/commands/topics/get.test.ts +125 -0
  155. package/tests/commands/topics/list.test.ts +124 -0
  156. package/tests/commands/topics/update.test.ts +177 -0
  157. package/tests/commands/webhooks/create.test.ts +224 -0
  158. package/tests/commands/webhooks/delete.test.ts +156 -0
  159. package/tests/commands/webhooks/get.test.ts +125 -0
  160. package/tests/commands/webhooks/list.test.ts +177 -0
  161. package/tests/commands/webhooks/update.test.ts +206 -0
  162. package/tests/commands/whoami.test.ts +99 -0
  163. package/tests/helpers.ts +93 -0
  164. package/tests/lib/client.test.ts +71 -0
  165. package/tests/lib/config.test.ts +414 -0
  166. package/tests/lib/files.test.ts +65 -0
  167. package/tests/lib/help-text.test.ts +96 -0
  168. package/tests/lib/output.test.ts +127 -0
  169. package/tests/lib/prompts.test.ts +178 -0
  170. package/tests/lib/spinner.test.ts +146 -0
  171. package/tests/lib/table.test.ts +63 -0
  172. package/tests/lib/tty.test.ts +85 -0
  173. package/tsconfig.json +14 -0
  174. package/src/index.js +0 -72
  175. package/src/routes.js +0 -37
  176. package/src/sections/apikeys.js +0 -99
  177. package/src/sections/audiences.js +0 -84
  178. package/src/sections/contacts.js +0 -177
  179. package/src/sections/domain.js +0 -195
  180. package/src/sections/email.js +0 -132
@@ -0,0 +1,168 @@
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
+ has_more: false,
23
+ data: [
24
+ {
25
+ id: 'attach_abc123',
26
+ filename: 'invoice.pdf',
27
+ size: 51200,
28
+ content_type: 'application/pdf',
29
+ content_disposition: 'attachment' as const,
30
+ content_id: null,
31
+ download_url: 'https://storage.example.com/signed/invoice.pdf',
32
+ expires_at: '2026-02-18T13:00:00.000Z',
33
+ },
34
+ ],
35
+ },
36
+ error: null,
37
+ }));
38
+
39
+ mock.module('resend', () => ({
40
+ Resend: class MockResend {
41
+ constructor(public key: string) {}
42
+ emails = { receiving: { attachments: { list: mockList } } };
43
+ },
44
+ }));
45
+
46
+ describe('emails receiving attachments command', () => {
47
+ const restoreEnv = captureTestEnv();
48
+ let spies: ReturnType<typeof setupOutputSpies> | undefined;
49
+ let errorSpy: ReturnType<typeof spyOn> | undefined;
50
+ let stderrSpy: ReturnType<typeof spyOn> | undefined;
51
+ let exitSpy: ReturnType<typeof spyOn> | undefined;
52
+
53
+ beforeEach(() => {
54
+ process.env.RESEND_API_KEY = 're_test_key';
55
+ mockList.mockClear();
56
+ });
57
+
58
+ afterEach(() => {
59
+ restoreEnv();
60
+ spies?.restore();
61
+ errorSpy?.mockRestore();
62
+ stderrSpy?.mockRestore();
63
+ exitSpy?.mockRestore();
64
+ spies = undefined;
65
+ errorSpy = undefined;
66
+ stderrSpy = undefined;
67
+ exitSpy = undefined;
68
+ });
69
+
70
+ test('calls SDK list with emailId and default pagination', async () => {
71
+ spies = setupOutputSpies();
72
+
73
+ const { listAttachmentsCommand } = await import(
74
+ '../../../../src/commands/emails/receiving/attachments'
75
+ );
76
+ await listAttachmentsCommand.parseAsync(['rcv_email123'], { from: 'user' });
77
+
78
+ expect(mockList).toHaveBeenCalledTimes(1);
79
+ const args = mockList.mock.calls[0][0] as Record<string, unknown>;
80
+ expect(args.emailId).toBe('rcv_email123');
81
+ expect(args.limit).toBe(10);
82
+ });
83
+
84
+ test('passes --limit to pagination options', async () => {
85
+ spies = setupOutputSpies();
86
+
87
+ const { listAttachmentsCommand } = await import(
88
+ '../../../../src/commands/emails/receiving/attachments'
89
+ );
90
+ await listAttachmentsCommand.parseAsync(['rcv_email123', '--limit', '5'], {
91
+ from: 'user',
92
+ });
93
+
94
+ const args = mockList.mock.calls[0][0] as Record<string, unknown>;
95
+ expect(args.limit).toBe(5);
96
+ });
97
+
98
+ test('outputs JSON list with attachment data when non-interactive', async () => {
99
+ spies = setupOutputSpies();
100
+
101
+ const { listAttachmentsCommand } = await import(
102
+ '../../../../src/commands/emails/receiving/attachments'
103
+ );
104
+ await listAttachmentsCommand.parseAsync(['rcv_email123'], { from: 'user' });
105
+
106
+ const output = spies.logSpy.mock.calls[0][0] as string;
107
+ const parsed = JSON.parse(output);
108
+ expect(Array.isArray(parsed.data)).toBe(true);
109
+ expect(parsed.data[0].id).toBe('attach_abc123');
110
+ expect(parsed.data[0].filename).toBe('invoice.pdf');
111
+ expect(parsed.data[0].content_type).toBe('application/pdf');
112
+ expect(parsed.has_more).toBe(false);
113
+ });
114
+
115
+ test('errors with invalid_limit for out-of-range limit', async () => {
116
+ setNonInteractive();
117
+ errorSpy = spyOn(console, 'error').mockImplementation(() => {});
118
+ exitSpy = mockExitThrow();
119
+
120
+ const { listAttachmentsCommand } = await import(
121
+ '../../../../src/commands/emails/receiving/attachments'
122
+ );
123
+ await expectExit1(() =>
124
+ listAttachmentsCommand.parseAsync(['rcv_email123', '--limit', '200'], {
125
+ from: 'user',
126
+ }),
127
+ );
128
+
129
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
130
+ expect(output).toContain('invalid_limit');
131
+ });
132
+
133
+ test('errors with auth_error when no API key', async () => {
134
+ setNonInteractive();
135
+ delete process.env.RESEND_API_KEY;
136
+ process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
137
+ errorSpy = spyOn(console, 'error').mockImplementation(() => {});
138
+ exitSpy = mockExitThrow();
139
+
140
+ const { listAttachmentsCommand } = await import(
141
+ '../../../../src/commands/emails/receiving/attachments'
142
+ );
143
+ await expectExit1(() =>
144
+ listAttachmentsCommand.parseAsync(['rcv_email123'], { from: 'user' }),
145
+ );
146
+
147
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
148
+ expect(output).toContain('auth_error');
149
+ });
150
+
151
+ test('errors with list_error when SDK returns an error', async () => {
152
+ setNonInteractive();
153
+ mockList.mockResolvedValueOnce(mockSdkError('Not found', 'not_found'));
154
+ errorSpy = spyOn(console, 'error').mockImplementation(() => {});
155
+ stderrSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
156
+ exitSpy = mockExitThrow();
157
+
158
+ const { listAttachmentsCommand } = await import(
159
+ '../../../../src/commands/emails/receiving/attachments'
160
+ );
161
+ await expectExit1(() =>
162
+ listAttachmentsCommand.parseAsync(['rcv_nonexistent'], { from: 'user' }),
163
+ );
164
+
165
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
166
+ expect(output).toContain('list_error');
167
+ });
168
+ });
@@ -0,0 +1,140 @@
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: 'email' as const,
22
+ id: 'rcv_abc123',
23
+ to: ['inbox@yourdomain.com'],
24
+ from: 'sender@external.com',
25
+ subject: 'Hello from outside',
26
+ created_at: '2026-02-18T12:00:00.000Z',
27
+ bcc: null,
28
+ cc: null,
29
+ reply_to: null,
30
+ html: '<p>Hello!</p>',
31
+ text: 'Hello!',
32
+ headers: { 'x-mailer': 'Thunderbird' },
33
+ message_id: '<hello@external.com>',
34
+ raw: {
35
+ download_url: 'https://storage.example.com/signed/raw-email',
36
+ expires_at: '2026-02-18T13:00:00.000Z',
37
+ },
38
+ attachments: [],
39
+ },
40
+ error: null,
41
+ }));
42
+
43
+ mock.module('resend', () => ({
44
+ Resend: class MockResend {
45
+ constructor(public key: string) {}
46
+ emails = { receiving: { get: mockGet } };
47
+ },
48
+ }));
49
+
50
+ describe('emails receiving get command', () => {
51
+ const restoreEnv = captureTestEnv();
52
+ let spies: ReturnType<typeof setupOutputSpies> | undefined;
53
+ let errorSpy: ReturnType<typeof spyOn> | undefined;
54
+ let stderrSpy: ReturnType<typeof spyOn> | undefined;
55
+ let exitSpy: ReturnType<typeof spyOn> | undefined;
56
+
57
+ beforeEach(() => {
58
+ process.env.RESEND_API_KEY = 're_test_key';
59
+ mockGet.mockClear();
60
+ });
61
+
62
+ afterEach(() => {
63
+ restoreEnv();
64
+ spies?.restore();
65
+ errorSpy?.mockRestore();
66
+ stderrSpy?.mockRestore();
67
+ exitSpy?.mockRestore();
68
+ spies = undefined;
69
+ errorSpy = undefined;
70
+ stderrSpy = undefined;
71
+ exitSpy = undefined;
72
+ });
73
+
74
+ test('calls SDK get with the provided id', async () => {
75
+ spies = setupOutputSpies();
76
+
77
+ const { getReceivingCommand } = await import(
78
+ '../../../../src/commands/emails/receiving/get'
79
+ );
80
+ await getReceivingCommand.parseAsync(['rcv_abc123'], { from: 'user' });
81
+
82
+ expect(mockGet).toHaveBeenCalledTimes(1);
83
+ expect(mockGet.mock.calls[0][0]).toBe('rcv_abc123');
84
+ });
85
+
86
+ test('outputs JSON with full email fields when non-interactive', async () => {
87
+ spies = setupOutputSpies();
88
+
89
+ const { getReceivingCommand } = await import(
90
+ '../../../../src/commands/emails/receiving/get'
91
+ );
92
+ await getReceivingCommand.parseAsync(['rcv_abc123'], { from: 'user' });
93
+
94
+ const output = spies.logSpy.mock.calls[0][0] as string;
95
+ const parsed = JSON.parse(output);
96
+ expect(parsed.id).toBe('rcv_abc123');
97
+ expect(parsed.from).toBe('sender@external.com');
98
+ expect(parsed.subject).toBe('Hello from outside');
99
+ expect(parsed.text).toBe('Hello!');
100
+ expect(parsed.raw.download_url).toBe(
101
+ 'https://storage.example.com/signed/raw-email',
102
+ );
103
+ });
104
+
105
+ test('errors with auth_error when no API key', async () => {
106
+ setNonInteractive();
107
+ delete process.env.RESEND_API_KEY;
108
+ process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
109
+ errorSpy = spyOn(console, 'error').mockImplementation(() => {});
110
+ exitSpy = mockExitThrow();
111
+
112
+ const { getReceivingCommand } = await import(
113
+ '../../../../src/commands/emails/receiving/get'
114
+ );
115
+ await expectExit1(() =>
116
+ getReceivingCommand.parseAsync(['rcv_abc123'], { from: 'user' }),
117
+ );
118
+
119
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
120
+ expect(output).toContain('auth_error');
121
+ });
122
+
123
+ test('errors with fetch_error when SDK returns an error', async () => {
124
+ setNonInteractive();
125
+ mockGet.mockResolvedValueOnce(mockSdkError('Not found', 'not_found'));
126
+ errorSpy = spyOn(console, 'error').mockImplementation(() => {});
127
+ stderrSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
128
+ exitSpy = mockExitThrow();
129
+
130
+ const { getReceivingCommand } = await import(
131
+ '../../../../src/commands/emails/receiving/get'
132
+ );
133
+ await expectExit1(() =>
134
+ getReceivingCommand.parseAsync(['rcv_nonexistent'], { from: 'user' }),
135
+ );
136
+
137
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
138
+ expect(output).toContain('fetch_error');
139
+ });
140
+ });
@@ -0,0 +1,181 @@
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
+ has_more: false,
23
+ data: [
24
+ {
25
+ id: 'rcv_abc123',
26
+ to: ['inbox@yourdomain.com'],
27
+ from: 'sender@external.com',
28
+ subject: 'Hello from outside',
29
+ created_at: '2026-02-18T12:00:00.000Z',
30
+ message_id: '<hello@external.com>',
31
+ bcc: null,
32
+ cc: null,
33
+ reply_to: null,
34
+ attachments: [],
35
+ },
36
+ ],
37
+ },
38
+ error: null,
39
+ }));
40
+
41
+ mock.module('resend', () => ({
42
+ Resend: class MockResend {
43
+ constructor(public key: string) {}
44
+ emails = { receiving: { list: mockList } };
45
+ },
46
+ }));
47
+
48
+ describe('emails receiving list command', () => {
49
+ const restoreEnv = captureTestEnv();
50
+ let spies: ReturnType<typeof setupOutputSpies> | undefined;
51
+ let errorSpy: ReturnType<typeof spyOn> | undefined;
52
+ let stderrSpy: ReturnType<typeof spyOn> | undefined;
53
+ let exitSpy: ReturnType<typeof spyOn> | undefined;
54
+
55
+ beforeEach(() => {
56
+ process.env.RESEND_API_KEY = 're_test_key';
57
+ mockList.mockClear();
58
+ });
59
+
60
+ afterEach(() => {
61
+ restoreEnv();
62
+ spies?.restore();
63
+ errorSpy?.mockRestore();
64
+ stderrSpy?.mockRestore();
65
+ exitSpy?.mockRestore();
66
+ spies = undefined;
67
+ errorSpy = undefined;
68
+ stderrSpy = undefined;
69
+ exitSpy = undefined;
70
+ });
71
+
72
+ test('calls SDK list with default pagination', async () => {
73
+ spies = setupOutputSpies();
74
+
75
+ const { listReceivingCommand } = await import(
76
+ '../../../../src/commands/emails/receiving/list'
77
+ );
78
+ await listReceivingCommand.parseAsync([], { from: 'user' });
79
+
80
+ expect(mockList).toHaveBeenCalledTimes(1);
81
+ const args = mockList.mock.calls[0][0] as Record<string, unknown>;
82
+ expect(args.limit).toBe(10);
83
+ });
84
+
85
+ test('passes --limit to pagination options', async () => {
86
+ spies = setupOutputSpies();
87
+
88
+ const { listReceivingCommand } = await import(
89
+ '../../../../src/commands/emails/receiving/list'
90
+ );
91
+ await listReceivingCommand.parseAsync(['--limit', '5'], { from: 'user' });
92
+
93
+ const args = mockList.mock.calls[0][0] as Record<string, unknown>;
94
+ expect(args.limit).toBe(5);
95
+ });
96
+
97
+ test('passes --after cursor to pagination options', async () => {
98
+ spies = setupOutputSpies();
99
+
100
+ const { listReceivingCommand } = await import(
101
+ '../../../../src/commands/emails/receiving/list'
102
+ );
103
+ await listReceivingCommand.parseAsync(['--after', 'rcv_cursor123'], {
104
+ from: 'user',
105
+ });
106
+
107
+ const args = mockList.mock.calls[0][0] as Record<string, unknown>;
108
+ expect(args.after).toBe('rcv_cursor123');
109
+ });
110
+
111
+ test('outputs JSON list with received email data when non-interactive', async () => {
112
+ spies = setupOutputSpies();
113
+
114
+ const { listReceivingCommand } = await import(
115
+ '../../../../src/commands/emails/receiving/list'
116
+ );
117
+ await listReceivingCommand.parseAsync([], { from: 'user' });
118
+
119
+ const output = spies.logSpy.mock.calls[0][0] as string;
120
+ const parsed = JSON.parse(output);
121
+ expect(Array.isArray(parsed.data)).toBe(true);
122
+ expect(parsed.data[0].id).toBe('rcv_abc123');
123
+ expect(parsed.data[0].from).toBe('sender@external.com');
124
+ expect(parsed.data[0].subject).toBe('Hello from outside');
125
+ expect(parsed.has_more).toBe(false);
126
+ });
127
+
128
+ test('errors with invalid_limit for out-of-range limit', async () => {
129
+ setNonInteractive();
130
+ errorSpy = spyOn(console, 'error').mockImplementation(() => {});
131
+ exitSpy = mockExitThrow();
132
+
133
+ const { listReceivingCommand } = await import(
134
+ '../../../../src/commands/emails/receiving/list'
135
+ );
136
+ await expectExit1(() =>
137
+ listReceivingCommand.parseAsync(['--limit', '200'], { from: 'user' }),
138
+ );
139
+
140
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
141
+ expect(output).toContain('invalid_limit');
142
+ });
143
+
144
+ test('errors with auth_error when no API key', async () => {
145
+ setNonInteractive();
146
+ delete process.env.RESEND_API_KEY;
147
+ process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
148
+ errorSpy = spyOn(console, 'error').mockImplementation(() => {});
149
+ exitSpy = mockExitThrow();
150
+
151
+ const { listReceivingCommand } = await import(
152
+ '../../../../src/commands/emails/receiving/list'
153
+ );
154
+ await expectExit1(() =>
155
+ listReceivingCommand.parseAsync([], { from: 'user' }),
156
+ );
157
+
158
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
159
+ expect(output).toContain('auth_error');
160
+ });
161
+
162
+ test('errors with list_error when SDK returns an error', async () => {
163
+ setNonInteractive();
164
+ mockList.mockResolvedValueOnce(
165
+ mockSdkError('Server error', 'server_error'),
166
+ );
167
+ errorSpy = spyOn(console, 'error').mockImplementation(() => {});
168
+ stderrSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
169
+ exitSpy = mockExitThrow();
170
+
171
+ const { listReceivingCommand } = await import(
172
+ '../../../../src/commands/emails/receiving/list'
173
+ );
174
+ await expectExit1(() =>
175
+ listReceivingCommand.parseAsync([], { from: 'user' }),
176
+ );
177
+
178
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
179
+ expect(output).toContain('list_error');
180
+ });
181
+ });