apple-notes-mcp 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.
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Integration Tests for Apple Notes MCP Server
3
+ *
4
+ * These tests verify the MCP tool handlers work correctly.
5
+ * The AppleNotesManager is mocked to test tool response formatting
6
+ * and error handling without requiring actual AppleScript execution.
7
+ */
8
+ import { describe, it, expect, vi, beforeEach } from "vitest";
9
+ import { AppleNotesManager } from "./services/appleNotesManager.js";
10
+ // Mock the AppleNotesManager class
11
+ vi.mock("@/services/appleNotesManager.js", () => {
12
+ return {
13
+ AppleNotesManager: vi.fn().mockImplementation(() => ({
14
+ createNote: vi.fn(),
15
+ searchNotes: vi.fn(),
16
+ getNoteContent: vi.fn(),
17
+ getNoteById: vi.fn(),
18
+ getNoteDetails: vi.fn(),
19
+ updateNote: vi.fn(),
20
+ deleteNote: vi.fn(),
21
+ moveNote: vi.fn(),
22
+ listNotes: vi.fn(),
23
+ listFolders: vi.fn(),
24
+ createFolder: vi.fn(),
25
+ deleteFolder: vi.fn(),
26
+ listAccounts: vi.fn(),
27
+ })),
28
+ };
29
+ });
30
+ // Create a typed mock manager for testing
31
+ const createMockManager = () => {
32
+ const manager = new AppleNotesManager();
33
+ return manager;
34
+ };
35
+ let mockManager;
36
+ // =============================================================================
37
+ // Response Helper Tests
38
+ // =============================================================================
39
+ describe("Response Helpers", () => {
40
+ // We test these indirectly through the tool handlers since they're not exported
41
+ // The pattern we're looking for is consistent response formatting
42
+ beforeEach(() => {
43
+ vi.clearAllMocks();
44
+ mockManager = createMockManager();
45
+ });
46
+ describe("success response format", () => {
47
+ it("returns content array with text type", async () => {
48
+ mockManager.createNote.mockReturnValue({
49
+ id: "123",
50
+ title: "Test Note",
51
+ content: "Test content",
52
+ tags: [],
53
+ created: new Date(),
54
+ modified: new Date(),
55
+ account: "iCloud",
56
+ });
57
+ // Simulate what the tool handler would return
58
+ const response = {
59
+ content: [{ type: "text", text: 'Note created: "Test Note"' }],
60
+ };
61
+ expect(response.content).toHaveLength(1);
62
+ expect(response.content[0].type).toBe("text");
63
+ expect(response.content[0].text).toContain("Note created");
64
+ });
65
+ });
66
+ describe("error response format", () => {
67
+ it("includes isError flag", () => {
68
+ const errorResponse = {
69
+ content: [{ type: "text", text: "Failed to create note" }],
70
+ isError: true,
71
+ };
72
+ expect(errorResponse.isError).toBe(true);
73
+ expect(errorResponse.content[0].text).toContain("Failed");
74
+ });
75
+ });
76
+ });
77
+ // =============================================================================
78
+ // Tool Handler Tests
79
+ // =============================================================================
80
+ describe("Tool Handlers", () => {
81
+ beforeEach(() => {
82
+ vi.clearAllMocks();
83
+ mockManager = createMockManager();
84
+ });
85
+ // ---------------------------------------------------------------------------
86
+ // create-note
87
+ // ---------------------------------------------------------------------------
88
+ describe("create-note tool", () => {
89
+ it("calls createNote with correct parameters", () => {
90
+ const params = {
91
+ title: "Shopping List",
92
+ content: "Eggs, Milk, Bread",
93
+ tags: ["groceries"],
94
+ };
95
+ mockManager.createNote.mockReturnValue({
96
+ id: "123",
97
+ title: params.title,
98
+ content: params.content,
99
+ tags: params.tags,
100
+ created: new Date(),
101
+ modified: new Date(),
102
+ account: "iCloud",
103
+ });
104
+ // Simulate the tool handler call
105
+ const result = mockManager.createNote(params.title, params.content, params.tags);
106
+ expect(mockManager.createNote).toHaveBeenCalledWith("Shopping List", "Eggs, Milk, Bread", [
107
+ "groceries",
108
+ ]);
109
+ expect(result).not.toBeNull();
110
+ });
111
+ it("handles creation failure gracefully", () => {
112
+ mockManager.createNote.mockReturnValue(null);
113
+ const result = mockManager.createNote("Test", "Content", []);
114
+ expect(result).toBeNull();
115
+ });
116
+ });
117
+ // ---------------------------------------------------------------------------
118
+ // search-notes
119
+ // ---------------------------------------------------------------------------
120
+ describe("search-notes tool", () => {
121
+ it("returns list of matching note titles", () => {
122
+ mockManager.searchNotes.mockReturnValue([
123
+ { title: "Meeting Notes", id: "1" },
124
+ { title: "Project Notes", id: "2" },
125
+ ]);
126
+ const results = mockManager.searchNotes("notes", false, undefined);
127
+ expect(results).toHaveLength(2);
128
+ expect(results[0].title).toBe("Meeting Notes");
129
+ });
130
+ it("handles empty search results", () => {
131
+ mockManager.searchNotes.mockReturnValue([]);
132
+ const results = mockManager.searchNotes("nonexistent", false);
133
+ expect(results).toHaveLength(0);
134
+ });
135
+ it("supports content search", () => {
136
+ mockManager.searchNotes.mockReturnValue([{ title: "Found Note", id: "1" }]);
137
+ mockManager.searchNotes("keyword", true, "iCloud");
138
+ expect(mockManager.searchNotes).toHaveBeenCalledWith("keyword", true, "iCloud");
139
+ });
140
+ });
141
+ // ---------------------------------------------------------------------------
142
+ // get-note-content
143
+ // ---------------------------------------------------------------------------
144
+ describe("get-note-content tool", () => {
145
+ it("returns note HTML content", () => {
146
+ const htmlContent = "<div>Shopping List</div><div>- Eggs</div>";
147
+ mockManager.getNoteContent.mockReturnValue(htmlContent);
148
+ const result = mockManager.getNoteContent("Shopping List");
149
+ expect(result).toBe(htmlContent);
150
+ });
151
+ it("returns empty string for missing note", () => {
152
+ mockManager.getNoteContent.mockReturnValue("");
153
+ const result = mockManager.getNoteContent("Missing");
154
+ expect(result).toBe("");
155
+ });
156
+ });
157
+ // ---------------------------------------------------------------------------
158
+ // get-note-by-id
159
+ // ---------------------------------------------------------------------------
160
+ describe("get-note-by-id tool", () => {
161
+ it("returns note metadata as JSON", () => {
162
+ const note = {
163
+ id: "x-coredata://ABC123/ICNote/p100",
164
+ title: "Test Note",
165
+ content: "",
166
+ tags: [],
167
+ created: new Date("2025-01-01"),
168
+ modified: new Date("2025-01-02"),
169
+ shared: false,
170
+ passwordProtected: false,
171
+ };
172
+ mockManager.getNoteById.mockReturnValue(note);
173
+ const result = mockManager.getNoteById("x-coredata://ABC123/ICNote/p100");
174
+ expect(result).not.toBeNull();
175
+ expect(result?.id).toBe("x-coredata://ABC123/ICNote/p100");
176
+ expect(result?.title).toBe("Test Note");
177
+ });
178
+ it("returns null for invalid ID", () => {
179
+ mockManager.getNoteById.mockReturnValue(null);
180
+ const result = mockManager.getNoteById("invalid-id");
181
+ expect(result).toBeNull();
182
+ });
183
+ });
184
+ // ---------------------------------------------------------------------------
185
+ // get-note-details
186
+ // ---------------------------------------------------------------------------
187
+ describe("get-note-details tool", () => {
188
+ it("returns full note metadata", () => {
189
+ const note = {
190
+ id: "x-coredata://ABC/ICNote/p1",
191
+ title: "Project Plan",
192
+ content: "",
193
+ tags: [],
194
+ created: new Date(),
195
+ modified: new Date(),
196
+ shared: true,
197
+ passwordProtected: false,
198
+ account: "iCloud",
199
+ };
200
+ mockManager.getNoteDetails.mockReturnValue(note);
201
+ const result = mockManager.getNoteDetails("Project Plan");
202
+ expect(result?.shared).toBe(true);
203
+ expect(result?.account).toBe("iCloud");
204
+ });
205
+ });
206
+ // ---------------------------------------------------------------------------
207
+ // update-note
208
+ // ---------------------------------------------------------------------------
209
+ describe("update-note tool", () => {
210
+ it("returns true on successful update", () => {
211
+ mockManager.updateNote.mockReturnValue(true);
212
+ const result = mockManager.updateNote("Old Title", "New Title", "New content");
213
+ expect(result).toBe(true);
214
+ });
215
+ it("returns false when note not found", () => {
216
+ mockManager.updateNote.mockReturnValue(false);
217
+ const result = mockManager.updateNote("Missing", undefined, "Content");
218
+ expect(result).toBe(false);
219
+ });
220
+ it("preserves title when newTitle is undefined", () => {
221
+ mockManager.updateNote.mockReturnValue(true);
222
+ mockManager.updateNote("Keep Title", undefined, "Updated content");
223
+ expect(mockManager.updateNote).toHaveBeenCalledWith("Keep Title", undefined, "Updated content");
224
+ });
225
+ });
226
+ // ---------------------------------------------------------------------------
227
+ // delete-note
228
+ // ---------------------------------------------------------------------------
229
+ describe("delete-note tool", () => {
230
+ it("returns true on successful deletion", () => {
231
+ mockManager.deleteNote.mockReturnValue(true);
232
+ const result = mockManager.deleteNote("Old Note");
233
+ expect(result).toBe(true);
234
+ });
235
+ it("returns false when deletion fails", () => {
236
+ mockManager.deleteNote.mockReturnValue(false);
237
+ const result = mockManager.deleteNote("Protected Note");
238
+ expect(result).toBe(false);
239
+ });
240
+ });
241
+ // ---------------------------------------------------------------------------
242
+ // move-note
243
+ // ---------------------------------------------------------------------------
244
+ describe("move-note tool", () => {
245
+ it("returns true when move succeeds", () => {
246
+ mockManager.moveNote.mockReturnValue(true);
247
+ const result = mockManager.moveNote("My Note", "Archive");
248
+ expect(result).toBe(true);
249
+ expect(mockManager.moveNote).toHaveBeenCalledWith("My Note", "Archive");
250
+ });
251
+ it("returns false when source note not found", () => {
252
+ mockManager.moveNote.mockReturnValue(false);
253
+ const result = mockManager.moveNote("Missing", "Archive");
254
+ expect(result).toBe(false);
255
+ });
256
+ it("returns false when destination folder not found", () => {
257
+ mockManager.moveNote.mockReturnValue(false);
258
+ const result = mockManager.moveNote("My Note", "Nonexistent");
259
+ expect(result).toBe(false);
260
+ });
261
+ });
262
+ // ---------------------------------------------------------------------------
263
+ // list-notes
264
+ // ---------------------------------------------------------------------------
265
+ describe("list-notes tool", () => {
266
+ it("returns array of note titles", () => {
267
+ mockManager.listNotes.mockReturnValue(["Note A", "Note B", "Note C"]);
268
+ const result = mockManager.listNotes();
269
+ expect(result).toEqual(["Note A", "Note B", "Note C"]);
270
+ });
271
+ it("returns empty array when no notes", () => {
272
+ mockManager.listNotes.mockReturnValue([]);
273
+ const result = mockManager.listNotes();
274
+ expect(result).toEqual([]);
275
+ });
276
+ it("filters by folder when specified", () => {
277
+ mockManager.listNotes.mockReturnValue(["Work Note 1", "Work Note 2"]);
278
+ mockManager.listNotes("iCloud", "Work");
279
+ expect(mockManager.listNotes).toHaveBeenCalledWith("iCloud", "Work");
280
+ });
281
+ });
282
+ // ---------------------------------------------------------------------------
283
+ // list-folders
284
+ // ---------------------------------------------------------------------------
285
+ describe("list-folders tool", () => {
286
+ it("returns array of Folder objects", () => {
287
+ mockManager.listFolders.mockReturnValue([
288
+ { id: "", name: "Notes", account: "iCloud" },
289
+ { id: "", name: "Archive", account: "iCloud" },
290
+ ]);
291
+ const result = mockManager.listFolders();
292
+ expect(result).toHaveLength(2);
293
+ expect(result[0].name).toBe("Notes");
294
+ });
295
+ it("returns empty array when no folders", () => {
296
+ mockManager.listFolders.mockReturnValue([]);
297
+ const result = mockManager.listFolders();
298
+ expect(result).toEqual([]);
299
+ });
300
+ });
301
+ // ---------------------------------------------------------------------------
302
+ // create-folder
303
+ // ---------------------------------------------------------------------------
304
+ describe("create-folder tool", () => {
305
+ it("returns Folder object on success", () => {
306
+ mockManager.createFolder.mockReturnValue({
307
+ id: "folder-123",
308
+ name: "New Project",
309
+ account: "iCloud",
310
+ });
311
+ const result = mockManager.createFolder("New Project");
312
+ expect(result).not.toBeNull();
313
+ expect(result?.name).toBe("New Project");
314
+ });
315
+ it("returns null when folder already exists", () => {
316
+ mockManager.createFolder.mockReturnValue(null);
317
+ const result = mockManager.createFolder("Existing");
318
+ expect(result).toBeNull();
319
+ });
320
+ });
321
+ // ---------------------------------------------------------------------------
322
+ // delete-folder
323
+ // ---------------------------------------------------------------------------
324
+ describe("delete-folder tool", () => {
325
+ it("returns true on successful deletion", () => {
326
+ mockManager.deleteFolder.mockReturnValue(true);
327
+ const result = mockManager.deleteFolder("Empty Folder");
328
+ expect(result).toBe(true);
329
+ });
330
+ it("returns false when folder contains notes", () => {
331
+ mockManager.deleteFolder.mockReturnValue(false);
332
+ const result = mockManager.deleteFolder("Non-Empty");
333
+ expect(result).toBe(false);
334
+ });
335
+ });
336
+ // ---------------------------------------------------------------------------
337
+ // list-accounts
338
+ // ---------------------------------------------------------------------------
339
+ describe("list-accounts tool", () => {
340
+ it("returns array of Account objects", () => {
341
+ mockManager.listAccounts.mockReturnValue([
342
+ { name: "iCloud" },
343
+ { name: "Gmail" },
344
+ { name: "Exchange" },
345
+ ]);
346
+ const result = mockManager.listAccounts();
347
+ expect(result).toHaveLength(3);
348
+ expect(result[0].name).toBe("iCloud");
349
+ });
350
+ it("returns empty array when no accounts configured", () => {
351
+ mockManager.listAccounts.mockReturnValue([]);
352
+ const result = mockManager.listAccounts();
353
+ expect(result).toEqual([]);
354
+ });
355
+ });
356
+ });
357
+ // =============================================================================
358
+ // Error Handling Tests
359
+ // =============================================================================
360
+ describe("Error Handling", () => {
361
+ beforeEach(() => {
362
+ vi.clearAllMocks();
363
+ mockManager = createMockManager();
364
+ });
365
+ it("handles manager throwing exceptions", () => {
366
+ mockManager.createNote.mockImplementation(() => {
367
+ throw new Error("Unexpected error");
368
+ });
369
+ expect(() => mockManager.createNote("Test", "Content", [])).toThrow("Unexpected error");
370
+ });
371
+ it("handles null returns gracefully", () => {
372
+ mockManager.getNoteById.mockReturnValue(null);
373
+ mockManager.getNoteDetails.mockReturnValue(null);
374
+ mockManager.createNote.mockReturnValue(null);
375
+ mockManager.createFolder.mockReturnValue(null);
376
+ expect(mockManager.getNoteById("invalid")).toBeNull();
377
+ expect(mockManager.getNoteDetails("missing")).toBeNull();
378
+ expect(mockManager.createNote("fail", "content", [])).toBeNull();
379
+ expect(mockManager.createFolder("exists")).toBeNull();
380
+ });
381
+ it("handles empty string returns", () => {
382
+ mockManager.getNoteContent.mockReturnValue("");
383
+ expect(mockManager.getNoteContent("missing")).toBe("");
384
+ });
385
+ it("handles empty array returns", () => {
386
+ mockManager.searchNotes.mockReturnValue([]);
387
+ mockManager.listNotes.mockReturnValue([]);
388
+ mockManager.listFolders.mockReturnValue([]);
389
+ mockManager.listAccounts.mockReturnValue([]);
390
+ expect(mockManager.searchNotes("none")).toEqual([]);
391
+ expect(mockManager.listNotes()).toEqual([]);
392
+ expect(mockManager.listFolders()).toEqual([]);
393
+ expect(mockManager.listAccounts()).toEqual([]);
394
+ });
395
+ it("handles false returns for destructive operations", () => {
396
+ mockManager.deleteNote.mockReturnValue(false);
397
+ mockManager.deleteFolder.mockReturnValue(false);
398
+ mockManager.updateNote.mockReturnValue(false);
399
+ mockManager.moveNote.mockReturnValue(false);
400
+ expect(mockManager.deleteNote("protected")).toBe(false);
401
+ expect(mockManager.deleteFolder("non-empty")).toBe(false);
402
+ expect(mockManager.updateNote("missing", undefined, "content")).toBe(false);
403
+ expect(mockManager.moveNote("missing", "folder")).toBe(false);
404
+ });
405
+ });
406
+ // =============================================================================
407
+ // Input Validation Tests
408
+ // =============================================================================
409
+ describe("Input Validation", () => {
410
+ beforeEach(() => {
411
+ vi.clearAllMocks();
412
+ mockManager = createMockManager();
413
+ });
414
+ describe("title parameter", () => {
415
+ it("handles titles with special characters", () => {
416
+ mockManager.getNoteContent.mockReturnValue("content");
417
+ mockManager.getNoteContent('Note\'s "Title" with <special> chars');
418
+ expect(mockManager.getNoteContent).toHaveBeenCalledWith('Note\'s "Title" with <special> chars');
419
+ });
420
+ it("handles titles with unicode", () => {
421
+ mockManager.getNoteContent.mockReturnValue("content");
422
+ mockManager.getNoteContent("日本語ノート 🎉");
423
+ expect(mockManager.getNoteContent).toHaveBeenCalledWith("日本語ノート 🎉");
424
+ });
425
+ });
426
+ describe("account parameter", () => {
427
+ it("defaults to iCloud when not specified", () => {
428
+ mockManager.createNote.mockReturnValue({
429
+ id: "1",
430
+ title: "Test",
431
+ content: "Content",
432
+ tags: [],
433
+ created: new Date(),
434
+ modified: new Date(),
435
+ account: "iCloud",
436
+ });
437
+ const result = mockManager.createNote("Test", "Content", []);
438
+ expect(result?.account).toBe("iCloud");
439
+ });
440
+ it("uses specified account", () => {
441
+ mockManager.listNotes.mockReturnValue(["Gmail Note"]);
442
+ mockManager.listNotes("Gmail");
443
+ expect(mockManager.listNotes).toHaveBeenCalledWith("Gmail");
444
+ });
445
+ });
446
+ });