@1kbirds/chidori-mock-gmail 0.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.
- package/README.md +85 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +139 -0
- package/dist/client.d.ts +228 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +50 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/seed.d.ts +6 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +127 -0
- package/dist/service.d.ts +41 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +372 -0
- package/dist/state.d.ts +22 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +139 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/ui/GmailMockApp.d.ts +7 -0
- package/dist/ui/GmailMockApp.d.ts.map +1 -0
- package/dist/ui/GmailMockApp.js +93 -0
- package/dist/ui/dev.d.ts +2 -0
- package/dist/ui/dev.d.ts.map +1 -0
- package/dist/ui/dev.js +11 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/styles.css +340 -0
- package/package.json +56 -0
- package/src/__tests__/service.test.ts +120 -0
- package/src/api.ts +157 -0
- package/src/client.ts +54 -0
- package/src/errors.ts +29 -0
- package/src/index.ts +12 -0
- package/src/seed.ts +143 -0
- package/src/service.ts +405 -0
- package/src/state.ts +159 -0
- package/src/types.ts +149 -0
- package/src/ui/GmailMockApp.tsx +236 -0
- package/src/ui/dev.tsx +16 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/styles.css +340 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body {
|
|
6
|
+
margin: 0;
|
|
7
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
8
|
+
color: #202124;
|
|
9
|
+
background: #f6f8fc;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
button,
|
|
13
|
+
input,
|
|
14
|
+
textarea {
|
|
15
|
+
font: inherit;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.gmail-shell {
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
display: grid;
|
|
21
|
+
grid-template-columns: 248px 1fr;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.gmail-sidebar {
|
|
25
|
+
padding: 18px 14px;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
gap: 18px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.gmail-brand {
|
|
32
|
+
font-size: 23px;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
padding-left: 8px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.gmail-compose {
|
|
38
|
+
width: fit-content;
|
|
39
|
+
border: 0;
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
padding: 13px 20px;
|
|
42
|
+
background: #c2e7ff;
|
|
43
|
+
color: #001d35;
|
|
44
|
+
box-shadow: 0 1px 2px rgb(60 64 67 / 30%);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.gmail-sidebar nav,
|
|
49
|
+
.gmail-labels {
|
|
50
|
+
display: grid;
|
|
51
|
+
gap: 3px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.gmail-sidebar nav button,
|
|
55
|
+
.gmail-labels button {
|
|
56
|
+
border: 0;
|
|
57
|
+
border-radius: 0 18px 18px 0;
|
|
58
|
+
padding: 8px 12px;
|
|
59
|
+
background: transparent;
|
|
60
|
+
display: flex;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
text-align: left;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.gmail-sidebar nav button.active,
|
|
67
|
+
.gmail-labels button.active {
|
|
68
|
+
background: #d3e3fd;
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.gmail-labels h2 {
|
|
73
|
+
margin: 0;
|
|
74
|
+
padding: 0 12px;
|
|
75
|
+
color: #5f6368;
|
|
76
|
+
font-size: 13px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.gmail-account {
|
|
80
|
+
margin-top: auto;
|
|
81
|
+
display: grid;
|
|
82
|
+
gap: 5px;
|
|
83
|
+
padding: 10px;
|
|
84
|
+
color: #5f6368;
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.gmail-account strong {
|
|
89
|
+
color: #202124;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.gmail-account button,
|
|
93
|
+
.gmail-actions button,
|
|
94
|
+
.gmail-composer button {
|
|
95
|
+
border: 1px solid #dadce0;
|
|
96
|
+
background: #fff;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
padding: 7px 10px;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.gmail-main {
|
|
103
|
+
min-width: 0;
|
|
104
|
+
display: grid;
|
|
105
|
+
grid-template-rows: auto 1fr;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.gmail-toolbar {
|
|
109
|
+
min-height: 66px;
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-columns: minmax(260px, 680px) auto;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 18px;
|
|
114
|
+
padding: 12px 20px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.gmail-toolbar input {
|
|
118
|
+
border: 0;
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
background: #eaf1fb;
|
|
121
|
+
padding: 13px 16px;
|
|
122
|
+
outline: 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.gmail-toolbar div {
|
|
126
|
+
display: grid;
|
|
127
|
+
gap: 2px;
|
|
128
|
+
justify-self: end;
|
|
129
|
+
text-align: right;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.gmail-toolbar span {
|
|
133
|
+
color: #5f6368;
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.gmail-workspace {
|
|
138
|
+
min-height: 0;
|
|
139
|
+
display: grid;
|
|
140
|
+
grid-template-columns: minmax(280px, 420px) 1fr;
|
|
141
|
+
gap: 16px;
|
|
142
|
+
padding: 0 20px 20px 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.gmail-list,
|
|
146
|
+
.gmail-reader {
|
|
147
|
+
background: #fff;
|
|
148
|
+
border-radius: 8px;
|
|
149
|
+
border: 1px solid #e0e0e0;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.gmail-list {
|
|
154
|
+
display: grid;
|
|
155
|
+
align-content: start;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.gmail-list button {
|
|
159
|
+
border: 0;
|
|
160
|
+
border-bottom: 1px solid #f1f3f4;
|
|
161
|
+
background: #fff;
|
|
162
|
+
padding: 12px 14px;
|
|
163
|
+
text-align: left;
|
|
164
|
+
display: grid;
|
|
165
|
+
grid-template-columns: 1fr auto;
|
|
166
|
+
gap: 3px 10px;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.gmail-list button.selected {
|
|
171
|
+
background: #eef4ff;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.gmail-list button.unread strong,
|
|
175
|
+
.gmail-list button.unread span {
|
|
176
|
+
font-weight: 800;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.gmail-list strong,
|
|
180
|
+
.gmail-list small {
|
|
181
|
+
grid-column: 1 / -1;
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
white-space: nowrap;
|
|
184
|
+
text-overflow: ellipsis;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.gmail-list small,
|
|
188
|
+
.gmail-list time {
|
|
189
|
+
color: #5f6368;
|
|
190
|
+
font-size: 12px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.gmail-reader {
|
|
194
|
+
padding: 22px;
|
|
195
|
+
overflow: auto;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.gmail-reader-head {
|
|
199
|
+
display: flex;
|
|
200
|
+
justify-content: space-between;
|
|
201
|
+
gap: 18px;
|
|
202
|
+
border-bottom: 1px solid #f1f3f4;
|
|
203
|
+
padding-bottom: 16px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.gmail-reader h1 {
|
|
207
|
+
margin: 0;
|
|
208
|
+
font-size: 24px;
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.gmail-reader p {
|
|
213
|
+
color: #5f6368;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.gmail-actions {
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-wrap: wrap;
|
|
219
|
+
align-content: start;
|
|
220
|
+
gap: 8px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.gmail-thread {
|
|
224
|
+
display: grid;
|
|
225
|
+
gap: 12px;
|
|
226
|
+
margin-top: 16px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.gmail-thread-message {
|
|
230
|
+
border: 1px solid #e0e0e0;
|
|
231
|
+
border-radius: 8px;
|
|
232
|
+
padding: 14px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.gmail-thread-message header {
|
|
236
|
+
display: flex;
|
|
237
|
+
justify-content: space-between;
|
|
238
|
+
gap: 12px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.gmail-thread-message header div {
|
|
242
|
+
display: grid;
|
|
243
|
+
gap: 2px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.gmail-thread-message span,
|
|
247
|
+
.gmail-thread-message time {
|
|
248
|
+
color: #5f6368;
|
|
249
|
+
font-size: 12px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.gmail-thread-message p {
|
|
253
|
+
color: #202124;
|
|
254
|
+
white-space: pre-wrap;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.gmail-attachments {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-wrap: wrap;
|
|
260
|
+
gap: 8px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.gmail-attachments span {
|
|
264
|
+
border: 1px solid #dadce0;
|
|
265
|
+
border-radius: 6px;
|
|
266
|
+
padding: 7px 9px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.gmail-empty {
|
|
270
|
+
display: grid;
|
|
271
|
+
min-height: 300px;
|
|
272
|
+
place-items: center;
|
|
273
|
+
color: #5f6368;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.gmail-composer {
|
|
277
|
+
position: fixed;
|
|
278
|
+
right: 24px;
|
|
279
|
+
bottom: 0;
|
|
280
|
+
width: min(560px, calc(100vw - 32px));
|
|
281
|
+
background: #fff;
|
|
282
|
+
border-radius: 8px 8px 0 0;
|
|
283
|
+
box-shadow: 0 8px 28px rgb(60 64 67 / 32%);
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
display: grid;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.gmail-composer header {
|
|
289
|
+
background: #f2f6fc;
|
|
290
|
+
padding: 10px 12px;
|
|
291
|
+
display: flex;
|
|
292
|
+
justify-content: space-between;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.gmail-composer header button {
|
|
296
|
+
border: 0;
|
|
297
|
+
background: transparent;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.gmail-composer input,
|
|
301
|
+
.gmail-composer textarea {
|
|
302
|
+
border: 0;
|
|
303
|
+
border-bottom: 1px solid #f1f3f4;
|
|
304
|
+
padding: 10px 12px;
|
|
305
|
+
outline: 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.gmail-composer textarea {
|
|
309
|
+
min-height: 220px;
|
|
310
|
+
resize: vertical;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.gmail-composer footer {
|
|
314
|
+
padding: 10px 12px;
|
|
315
|
+
display: flex;
|
|
316
|
+
justify-content: flex-end;
|
|
317
|
+
gap: 8px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.gmail-composer .gmail-send {
|
|
321
|
+
background: #0b57d0;
|
|
322
|
+
color: #fff;
|
|
323
|
+
border-color: #0b57d0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@media (max-width: 860px) {
|
|
327
|
+
.gmail-shell,
|
|
328
|
+
.gmail-workspace {
|
|
329
|
+
grid-template-columns: 1fr;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.gmail-toolbar {
|
|
333
|
+
grid-template-columns: 1fr;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.gmail-toolbar div {
|
|
337
|
+
justify-self: start;
|
|
338
|
+
text-align: left;
|
|
339
|
+
}
|
|
340
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@1kbirds/chidori-mock-gmail",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Gmail mock app and tools for Chidori TypeScript agents.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./ui": {
|
|
15
|
+
"types": "./dist/ui/index.d.ts",
|
|
16
|
+
"import": "./dist/ui/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./ui/styles.css": "./dist/ui/styles.css"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json && cp src/ui/styles.css dist/ui/styles.css",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"dev": "vite --host 127.0.0.1 --port 5175"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": ">=18",
|
|
32
|
+
"react-dom": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.1.0",
|
|
36
|
+
"@types/react-dom": "^19.1.0",
|
|
37
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
38
|
+
"react": "^19.1.0",
|
|
39
|
+
"react-dom": "^19.1.0",
|
|
40
|
+
"typescript": "^5.8.0",
|
|
41
|
+
"vite": "^7.0.0",
|
|
42
|
+
"vitest": "^3.2.0"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/ThousandBirdsInc/chidori-mock.git",
|
|
50
|
+
"directory": "packages/gmail"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/ThousandBirdsInc/chidori-mock/tree/main/packages/gmail",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/ThousandBirdsInc/chidori-mock/issues"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createGmailClient, createGmailMock, demoSeed, handleGmailApiRequest } from "../index";
|
|
3
|
+
import { GmailMockError } from "../errors";
|
|
4
|
+
|
|
5
|
+
describe("GmailMock service", () => {
|
|
6
|
+
it("creates isolated seeded mock instances", () => {
|
|
7
|
+
const first = createGmailMock({ now: "2026-06-01T16:30:00.000Z", seed: demoSeed });
|
|
8
|
+
const second = createGmailMock({ now: "2026-06-01T16:30:00.000Z", seed: demoSeed });
|
|
9
|
+
|
|
10
|
+
first.send({ to: ["new@example.com"], subject: "Only first", body: "Hello" });
|
|
11
|
+
|
|
12
|
+
expect(first.listMessages({ labelIds: ["SENT"] }).resultSizeEstimate).toBe(2);
|
|
13
|
+
expect(second.listMessages({ labelIds: ["SENT"] }).resultSizeEstimate).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("lists, searches, reads, modifies, trashes, untrashes, and deletes messages", () => {
|
|
17
|
+
const mock = createGmailMock({ now: "2026-06-01T16:30:00.000Z", seed: demoSeed });
|
|
18
|
+
|
|
19
|
+
const inbox = mock.listMessages({ labelIds: ["INBOX"] });
|
|
20
|
+
expect(inbox.resultSizeEstimate).toBe(3);
|
|
21
|
+
|
|
22
|
+
const search = mock.listMessages({ q: "from:morgan@example.com is:unread" });
|
|
23
|
+
expect(search.messages?.[0].id).toBe("msg_schedule_request");
|
|
24
|
+
|
|
25
|
+
const read = mock.modifyMessage("msg_schedule_request", { removeLabelIds: ["UNREAD"] });
|
|
26
|
+
expect(read.labelIds).not.toContain("UNREAD");
|
|
27
|
+
|
|
28
|
+
const archived = mock.modifyMessage("msg_schedule_request", { removeLabelIds: ["INBOX"] });
|
|
29
|
+
expect(archived.labelIds).not.toContain("INBOX");
|
|
30
|
+
|
|
31
|
+
const trashed = mock.trashMessage("msg_schedule_request");
|
|
32
|
+
expect(trashed.labelIds).toContain("TRASH");
|
|
33
|
+
|
|
34
|
+
const untrashed = mock.untrashMessage("msg_schedule_request");
|
|
35
|
+
expect(untrashed.labelIds).toContain("INBOX");
|
|
36
|
+
|
|
37
|
+
mock.deleteMessage("msg_schedule_request");
|
|
38
|
+
expect(() => mock.getMessage("msg_schedule_request")).toThrow(GmailMockError);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("supports labels", () => {
|
|
42
|
+
const mock = createGmailMock({ seed: demoSeed });
|
|
43
|
+
|
|
44
|
+
const label = mock.createLabel({ name: "Follow Up" });
|
|
45
|
+
expect(label.type).toBe("user");
|
|
46
|
+
|
|
47
|
+
mock.modifyMessage("msg_launch_brief", { addLabelIds: [label.id] });
|
|
48
|
+
expect(mock.listMessages({ labelIds: [label.id] }).messages?.[0].id).toBe("msg_launch_brief");
|
|
49
|
+
|
|
50
|
+
mock.updateLabel(label.id, { name: "Follow-up" });
|
|
51
|
+
expect(mock.getLabel(label.id).name).toBe("Follow-up");
|
|
52
|
+
|
|
53
|
+
mock.deleteLabel(label.id);
|
|
54
|
+
expect(() => mock.getLabel(label.id)).toThrow(GmailMockError);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("groups threads and modifies a full thread", () => {
|
|
58
|
+
const mock = createGmailMock({ seed: demoSeed });
|
|
59
|
+
|
|
60
|
+
const thread = mock.getThread("thread_customer");
|
|
61
|
+
expect(thread.messages).toHaveLength(2);
|
|
62
|
+
|
|
63
|
+
mock.modifyThread("thread_customer", { addLabelIds: ["STARRED"] });
|
|
64
|
+
expect(mock.getThread("thread_customer").messages.every((message) => message.labelIds.includes("STARRED"))).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("supports drafts and send", () => {
|
|
68
|
+
const mock = createGmailMock({ now: "2026-06-01T16:30:00.000Z", seed: demoSeed });
|
|
69
|
+
|
|
70
|
+
const draft = mock.createDraft({ to: ["morgan@example.com"], subject: "Meeting", body: "Tuesday works." });
|
|
71
|
+
expect(mock.getDraft(draft.id).message.labelIds).toContain("DRAFT");
|
|
72
|
+
|
|
73
|
+
const updated = mock.updateDraft(draft.id, { to: ["morgan@example.com"], subject: "Meeting", body: "Wednesday works." });
|
|
74
|
+
expect(updated.message.snippet).toBe("Wednesday works.");
|
|
75
|
+
|
|
76
|
+
const sent = mock.sendDraft(draft.id);
|
|
77
|
+
expect(sent.labelIds).toEqual(["SENT"]);
|
|
78
|
+
expect(() => mock.getDraft(draft.id)).toThrow(GmailMockError);
|
|
79
|
+
|
|
80
|
+
const direct = mock.send({ to: ["grace@example.com"], subject: "Direct", body: "Sent directly." });
|
|
81
|
+
expect(direct.labelIds).toEqual(["SENT"]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("Gmail client facade", () => {
|
|
86
|
+
it("returns googleapis-like response wrappers", async () => {
|
|
87
|
+
const mock = createGmailMock({ seed: demoSeed });
|
|
88
|
+
const client = createGmailClient(mock);
|
|
89
|
+
|
|
90
|
+
const profile = await client.users.getProfile();
|
|
91
|
+
expect(profile.status).toBe(200);
|
|
92
|
+
expect(profile.data.emailAddress).toBe("ada@example.com");
|
|
93
|
+
|
|
94
|
+
const sent = await client.users.messages.send({ requestBody: { to: ["x@example.com"], subject: "Hi", body: "Hello" } });
|
|
95
|
+
expect(sent.data.labelIds).toEqual(["SENT"]);
|
|
96
|
+
|
|
97
|
+
const deleted = await client.users.messages.delete({ id: sent.data.id });
|
|
98
|
+
expect(deleted.status).toBe(204);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("Gmail REST-style adapter", () => {
|
|
103
|
+
it("routes common Gmail endpoints", () => {
|
|
104
|
+
const mock = createGmailMock({ seed: demoSeed });
|
|
105
|
+
|
|
106
|
+
const sent = handleGmailApiRequest(mock, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
path: "/gmail/v1/users/me/messages/send",
|
|
109
|
+
body: { to: ["api@example.com"], subject: "API", body: "Created through API" },
|
|
110
|
+
});
|
|
111
|
+
expect(sent.status).toBe(200);
|
|
112
|
+
|
|
113
|
+
const listed = handleGmailApiRequest(mock, {
|
|
114
|
+
method: "GET",
|
|
115
|
+
path: "/gmail/v1/users/me/messages",
|
|
116
|
+
query: { q: "subject:API", labelIds: ["SENT"] },
|
|
117
|
+
});
|
|
118
|
+
expect((listed.body as { resultSizeEstimate: number }).resultSizeEstimate).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { GmailMockError } from "./errors";
|
|
2
|
+
import type { GmailMock } from "./service";
|
|
3
|
+
import type { GmailLabel, GmailMessage, GmailModifyRequest, GmailSearchOptions, GmailSendRequest } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface GmailApiRequest {
|
|
6
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
7
|
+
path: string;
|
|
8
|
+
query?: Record<string, string | number | boolean | string[] | undefined>;
|
|
9
|
+
body?: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GmailApiResponse<T = unknown> {
|
|
13
|
+
status: number;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
body?: T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function handleGmailApiRequest(mock: GmailMock, request: GmailApiRequest): GmailApiResponse {
|
|
19
|
+
try {
|
|
20
|
+
return route(mock, request);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error instanceof GmailMockError) {
|
|
23
|
+
return json(error.status, error.toJSON());
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function route(mock: GmailMock, request: GmailApiRequest): GmailApiResponse {
|
|
30
|
+
const url = new URL(request.path, "https://gmail.googleapis.com");
|
|
31
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
32
|
+
const query = request.query ?? {};
|
|
33
|
+
|
|
34
|
+
if (request.method === "GET" && path === "/gmail/v1/users/me/profile") {
|
|
35
|
+
return json(200, mock.getProfile());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (path === "/gmail/v1/users/me/labels") {
|
|
39
|
+
if (request.method === "GET") return json(200, mock.listLabels());
|
|
40
|
+
if (request.method === "POST") return json(200, mock.createLabel(request.body as Partial<GmailLabel>));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const labelMatch = path.match(/^\/gmail\/v1\/users\/me\/labels\/([^/]+)$/);
|
|
44
|
+
if (labelMatch) {
|
|
45
|
+
const id = decodeURIComponent(labelMatch[1]);
|
|
46
|
+
if (request.method === "GET") return json(200, mock.getLabel(id));
|
|
47
|
+
if (request.method === "PUT" || request.method === "PATCH") return json(200, mock.updateLabel(id, request.body as Partial<GmailLabel>));
|
|
48
|
+
if (request.method === "DELETE") {
|
|
49
|
+
mock.deleteLabel(id);
|
|
50
|
+
return { status: 204, headers: {} };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (path === "/gmail/v1/users/me/messages") {
|
|
55
|
+
if (request.method === "GET") return json(200, mock.listMessages(searchOptions(query)));
|
|
56
|
+
if (request.method === "POST") return json(200, mock.insertMessage(request.body as Partial<GmailMessage> | GmailSendRequest));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (request.method === "POST" && path === "/gmail/v1/users/me/messages/send") {
|
|
60
|
+
return json(200, mock.send(request.body as GmailSendRequest));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const messageActionMatch = path.match(/^\/gmail\/v1\/users\/me\/messages\/([^/]+)\/(modify|trash|untrash)$/);
|
|
64
|
+
if (messageActionMatch && request.method === "POST") {
|
|
65
|
+
const id = decodeURIComponent(messageActionMatch[1]);
|
|
66
|
+
const action = messageActionMatch[2];
|
|
67
|
+
if (action === "modify") return json(200, mock.modifyMessage(id, request.body as GmailModifyRequest));
|
|
68
|
+
if (action === "trash") return json(200, mock.trashMessage(id));
|
|
69
|
+
return json(200, mock.untrashMessage(id));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const messageMatch = path.match(/^\/gmail\/v1\/users\/me\/messages\/([^/]+)$/);
|
|
73
|
+
if (messageMatch) {
|
|
74
|
+
const id = decodeURIComponent(messageMatch[1]);
|
|
75
|
+
if (request.method === "GET") return json(200, mock.getMessage(id));
|
|
76
|
+
if (request.method === "DELETE") {
|
|
77
|
+
mock.deleteMessage(id);
|
|
78
|
+
return { status: 204, headers: {} };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (path === "/gmail/v1/users/me/threads" && request.method === "GET") {
|
|
83
|
+
return json(200, mock.listThreads(searchOptions(query)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const threadActionMatch = path.match(/^\/gmail\/v1\/users\/me\/threads\/([^/]+)\/(modify|trash)$/);
|
|
87
|
+
if (threadActionMatch && request.method === "POST") {
|
|
88
|
+
const id = decodeURIComponent(threadActionMatch[1]);
|
|
89
|
+
return json(200, threadActionMatch[2] === "modify" ? mock.modifyThread(id, request.body as GmailModifyRequest) : mock.trashThread(id));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const threadMatch = path.match(/^\/gmail\/v1\/users\/me\/threads\/([^/]+)$/);
|
|
93
|
+
if (threadMatch && request.method === "GET") {
|
|
94
|
+
return json(200, mock.getThread(decodeURIComponent(threadMatch[1])));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (path === "/gmail/v1/users/me/drafts") {
|
|
98
|
+
if (request.method === "GET") return json(200, mock.listDrafts());
|
|
99
|
+
if (request.method === "POST") return json(200, mock.createDraft(request.body as GmailSendRequest));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const draftSendMatch = path.match(/^\/gmail\/v1\/users\/me\/drafts\/([^/]+)\/send$/);
|
|
103
|
+
if (draftSendMatch && request.method === "POST") return json(200, mock.sendDraft(decodeURIComponent(draftSendMatch[1])));
|
|
104
|
+
|
|
105
|
+
const draftMatch = path.match(/^\/gmail\/v1\/users\/me\/drafts\/([^/]+)$/);
|
|
106
|
+
if (draftMatch) {
|
|
107
|
+
const id = decodeURIComponent(draftMatch[1]);
|
|
108
|
+
if (request.method === "GET") return json(200, mock.getDraft(id));
|
|
109
|
+
if (request.method === "PUT") return json(200, mock.updateDraft(id, request.body as GmailSendRequest));
|
|
110
|
+
if (request.method === "DELETE") {
|
|
111
|
+
mock.deleteDraft(id);
|
|
112
|
+
return { status: 204, headers: {} };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return json(404, {
|
|
117
|
+
error: {
|
|
118
|
+
code: 404,
|
|
119
|
+
message: `Unsupported mock endpoint: ${request.method} ${request.path}`,
|
|
120
|
+
errors: [{ domain: "global", reason: "notFound", message: "Unsupported mock endpoint" }],
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function json<T>(status: number, body: T): GmailApiResponse<T> {
|
|
126
|
+
return { status, headers: { "content-type": "application/json" }, body };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function searchOptions(query: GmailApiRequest["query"]): GmailSearchOptions {
|
|
130
|
+
return {
|
|
131
|
+
q: stringQuery(query?.q),
|
|
132
|
+
labelIds: arrayQuery(query?.labelIds),
|
|
133
|
+
includeSpamTrash: boolQuery(query?.includeSpamTrash),
|
|
134
|
+
maxResults: numberQuery(query?.maxResults),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function stringQuery(value: string | number | boolean | string[] | undefined): string | undefined {
|
|
139
|
+
return Array.isArray(value) ? value[0] : value === undefined ? undefined : String(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function arrayQuery(value: string | number | boolean | string[] | undefined): string[] | undefined {
|
|
143
|
+
if (Array.isArray(value)) return value.map(String);
|
|
144
|
+
if (typeof value === "string") return value.split(",").filter(Boolean);
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function boolQuery(value: string | number | boolean | string[] | undefined): boolean | undefined {
|
|
149
|
+
if (value === undefined) return undefined;
|
|
150
|
+
return value === true || value === "true";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function numberQuery(value: string | number | boolean | string[] | undefined): number | undefined {
|
|
154
|
+
if (value === undefined || Array.isArray(value)) return undefined;
|
|
155
|
+
const parsed = Number(value);
|
|
156
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
157
|
+
}
|