@jgardner04/ghost-mcp-server 1.7.0 → 1.9.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/package.json +1 -1
- package/src/mcp_server_improved.js +117 -0
- package/src/schemas/__tests__/common.test.js +275 -0
- package/src/schemas/__tests__/postSchemas.test.js +194 -0
- package/src/schemas/__tests__/tagSchemas.test.js +262 -0
- package/src/schemas/common.js +227 -0
- package/src/schemas/index.js +39 -0
- package/src/schemas/memberSchemas.js +188 -0
- package/src/schemas/newsletterSchemas.js +168 -0
- package/src/schemas/pageSchemas.js +215 -0
- package/src/schemas/postSchemas.js +210 -0
- package/src/schemas/tagSchemas.js +136 -0
- package/src/schemas/tierSchemas.js +206 -0
- package/src/services/__tests__/ghostServiceImproved.members.test.js +261 -0
- package/src/services/__tests__/memberService.test.js +245 -0
- package/src/services/ghostServiceImproved.js +107 -0
- package/src/services/memberService.js +202 -0
package/package.json
CHANGED
|
@@ -903,6 +903,122 @@ server.tool(
|
|
|
903
903
|
}
|
|
904
904
|
);
|
|
905
905
|
|
|
906
|
+
// =============================================================================
|
|
907
|
+
// MEMBER TOOLS
|
|
908
|
+
// Member management for Ghost CMS subscribers
|
|
909
|
+
// =============================================================================
|
|
910
|
+
|
|
911
|
+
// Create Member Tool
|
|
912
|
+
server.tool(
|
|
913
|
+
'ghost_create_member',
|
|
914
|
+
'Creates a new member (subscriber) in Ghost CMS.',
|
|
915
|
+
{
|
|
916
|
+
email: z.string().email().describe('The email address of the member (required).'),
|
|
917
|
+
name: z.string().optional().describe('The name of the member.'),
|
|
918
|
+
note: z.string().optional().describe('A note about the member.'),
|
|
919
|
+
labels: z.array(z.string()).optional().describe('List of label names to assign to the member.'),
|
|
920
|
+
newsletters: z
|
|
921
|
+
.array(z.object({ id: z.string() }))
|
|
922
|
+
.optional()
|
|
923
|
+
.describe('List of newsletter objects with id field to subscribe the member to.'),
|
|
924
|
+
subscribed: z
|
|
925
|
+
.boolean()
|
|
926
|
+
.optional()
|
|
927
|
+
.describe('Whether the member is subscribed to emails. Defaults to true.'),
|
|
928
|
+
},
|
|
929
|
+
async (input) => {
|
|
930
|
+
console.error(`Executing tool: ghost_create_member with email: ${input.email}`);
|
|
931
|
+
try {
|
|
932
|
+
await loadServices();
|
|
933
|
+
|
|
934
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
935
|
+
const createdMember = await ghostServiceImproved.createMember(input);
|
|
936
|
+
console.error(`Member created successfully. Member ID: ${createdMember.id}`);
|
|
937
|
+
|
|
938
|
+
return {
|
|
939
|
+
content: [{ type: 'text', text: JSON.stringify(createdMember, null, 2) }],
|
|
940
|
+
};
|
|
941
|
+
} catch (error) {
|
|
942
|
+
console.error(`Error in ghost_create_member:`, error);
|
|
943
|
+
return {
|
|
944
|
+
content: [{ type: 'text', text: `Error creating member: ${error.message}` }],
|
|
945
|
+
isError: true,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
// Update Member Tool
|
|
952
|
+
server.tool(
|
|
953
|
+
'ghost_update_member',
|
|
954
|
+
'Updates an existing member in Ghost CMS. All fields except id are optional.',
|
|
955
|
+
{
|
|
956
|
+
id: z.string().describe('The ID of the member to update.'),
|
|
957
|
+
email: z.string().email().optional().describe('New email address for the member.'),
|
|
958
|
+
name: z.string().optional().describe('New name for the member.'),
|
|
959
|
+
note: z.string().optional().describe('New note about the member.'),
|
|
960
|
+
labels: z
|
|
961
|
+
.array(z.string())
|
|
962
|
+
.optional()
|
|
963
|
+
.describe('New list of label names to assign to the member.'),
|
|
964
|
+
newsletters: z
|
|
965
|
+
.array(z.object({ id: z.string() }))
|
|
966
|
+
.optional()
|
|
967
|
+
.describe('New list of newsletter objects with id field to subscribe the member to.'),
|
|
968
|
+
},
|
|
969
|
+
async (input) => {
|
|
970
|
+
console.error(`Executing tool: ghost_update_member for member ID: ${input.id}`);
|
|
971
|
+
try {
|
|
972
|
+
await loadServices();
|
|
973
|
+
|
|
974
|
+
const { id, ...updateData } = input;
|
|
975
|
+
|
|
976
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
977
|
+
const updatedMember = await ghostServiceImproved.updateMember(id, updateData);
|
|
978
|
+
console.error(`Member updated successfully. Member ID: ${updatedMember.id}`);
|
|
979
|
+
|
|
980
|
+
return {
|
|
981
|
+
content: [{ type: 'text', text: JSON.stringify(updatedMember, null, 2) }],
|
|
982
|
+
};
|
|
983
|
+
} catch (error) {
|
|
984
|
+
console.error(`Error in ghost_update_member:`, error);
|
|
985
|
+
return {
|
|
986
|
+
content: [{ type: 'text', text: `Error updating member: ${error.message}` }],
|
|
987
|
+
isError: true,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
// Delete Member Tool
|
|
994
|
+
server.tool(
|
|
995
|
+
'ghost_delete_member',
|
|
996
|
+
'Deletes a member from Ghost CMS by ID. This operation is permanent and cannot be undone.',
|
|
997
|
+
{
|
|
998
|
+
id: z.string().describe('The ID of the member to delete.'),
|
|
999
|
+
},
|
|
1000
|
+
async ({ id }) => {
|
|
1001
|
+
console.error(`Executing tool: ghost_delete_member for member ID: ${id}`);
|
|
1002
|
+
try {
|
|
1003
|
+
await loadServices();
|
|
1004
|
+
|
|
1005
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
1006
|
+
await ghostServiceImproved.deleteMember(id);
|
|
1007
|
+
console.error(`Member deleted successfully. Member ID: ${id}`);
|
|
1008
|
+
|
|
1009
|
+
return {
|
|
1010
|
+
content: [{ type: 'text', text: `Member ${id} has been successfully deleted.` }],
|
|
1011
|
+
};
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
console.error(`Error in ghost_delete_member:`, error);
|
|
1014
|
+
return {
|
|
1015
|
+
content: [{ type: 'text', text: `Error deleting member: ${error.message}` }],
|
|
1016
|
+
isError: true,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
);
|
|
1021
|
+
|
|
906
1022
|
// =============================================================================
|
|
907
1023
|
// NEWSLETTER TOOLS
|
|
908
1024
|
// =============================================================================
|
|
@@ -1108,6 +1224,7 @@ async function main() {
|
|
|
1108
1224
|
'Available tools: ghost_get_tags, ghost_create_tag, ghost_get_tag, ghost_update_tag, ghost_delete_tag, ghost_upload_image, ' +
|
|
1109
1225
|
'ghost_create_post, ghost_get_posts, ghost_get_post, ghost_search_posts, ghost_update_post, ghost_delete_post, ' +
|
|
1110
1226
|
'ghost_get_pages, ghost_get_page, ghost_create_page, ghost_update_page, ghost_delete_page, ghost_search_pages, ' +
|
|
1227
|
+
'ghost_create_member, ghost_update_member, ghost_delete_member, ' +
|
|
1111
1228
|
'ghost_get_newsletters, ghost_get_newsletter, ghost_create_newsletter, ghost_update_newsletter, ghost_delete_newsletter'
|
|
1112
1229
|
);
|
|
1113
1230
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
emailSchema,
|
|
4
|
+
urlSchema,
|
|
5
|
+
isoDateSchema,
|
|
6
|
+
slugSchema,
|
|
7
|
+
ghostIdSchema,
|
|
8
|
+
nqlFilterSchema,
|
|
9
|
+
limitSchema,
|
|
10
|
+
pageSchema,
|
|
11
|
+
postStatusSchema,
|
|
12
|
+
visibilitySchema,
|
|
13
|
+
htmlContentSchema,
|
|
14
|
+
titleSchema,
|
|
15
|
+
excerptSchema,
|
|
16
|
+
metaTitleSchema,
|
|
17
|
+
metaDescriptionSchema,
|
|
18
|
+
featuredSchema,
|
|
19
|
+
featureImageSchema,
|
|
20
|
+
featureImageAltSchema,
|
|
21
|
+
tagNameSchema,
|
|
22
|
+
} from '../common.js';
|
|
23
|
+
|
|
24
|
+
describe('Common Schemas', () => {
|
|
25
|
+
describe('emailSchema', () => {
|
|
26
|
+
it('should accept valid email addresses', () => {
|
|
27
|
+
expect(() => emailSchema.parse('test@example.com')).not.toThrow();
|
|
28
|
+
expect(() => emailSchema.parse('user.name+tag@example.co.uk')).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should reject invalid email addresses', () => {
|
|
32
|
+
expect(() => emailSchema.parse('not-an-email')).toThrow();
|
|
33
|
+
expect(() => emailSchema.parse('missing@domain')).toThrow();
|
|
34
|
+
expect(() => emailSchema.parse('@example.com')).toThrow();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('urlSchema', () => {
|
|
39
|
+
it('should accept valid URLs', () => {
|
|
40
|
+
expect(() => urlSchema.parse('https://example.com')).not.toThrow();
|
|
41
|
+
expect(() => urlSchema.parse('http://localhost:3000/path')).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should reject invalid URLs', () => {
|
|
45
|
+
expect(() => urlSchema.parse('not-a-url')).toThrow();
|
|
46
|
+
expect(() => urlSchema.parse('://invalid')).toThrow();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('isoDateSchema', () => {
|
|
51
|
+
it('should accept valid ISO 8601 datetime strings', () => {
|
|
52
|
+
expect(() => isoDateSchema.parse('2024-01-15T10:30:00Z')).not.toThrow();
|
|
53
|
+
expect(() => isoDateSchema.parse('2024-01-15T10:30:00.000Z')).not.toThrow();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should reject invalid datetime strings', () => {
|
|
57
|
+
expect(() => isoDateSchema.parse('2024-01-15')).toThrow();
|
|
58
|
+
expect(() => isoDateSchema.parse('not-a-date')).toThrow();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('slugSchema', () => {
|
|
63
|
+
it('should accept valid slugs', () => {
|
|
64
|
+
expect(() => slugSchema.parse('my-blog-post')).not.toThrow();
|
|
65
|
+
expect(() => slugSchema.parse('post-123')).not.toThrow();
|
|
66
|
+
expect(() => slugSchema.parse('simple')).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should reject invalid slugs', () => {
|
|
70
|
+
expect(() => slugSchema.parse('My Post')).toThrow(); // spaces
|
|
71
|
+
expect(() => slugSchema.parse('post_123')).toThrow(); // underscores
|
|
72
|
+
expect(() => slugSchema.parse('Post-123')).toThrow(); // uppercase
|
|
73
|
+
expect(() => slugSchema.parse('post!')).toThrow(); // special chars
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('ghostIdSchema', () => {
|
|
78
|
+
it('should accept valid Ghost IDs', () => {
|
|
79
|
+
expect(() => ghostIdSchema.parse('507f1f77bcf86cd799439011')).not.toThrow();
|
|
80
|
+
expect(() => ghostIdSchema.parse('abcdef1234567890abcdef12')).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should reject invalid Ghost IDs', () => {
|
|
84
|
+
expect(() => ghostIdSchema.parse('short')).toThrow(); // too short
|
|
85
|
+
expect(() => ghostIdSchema.parse('507f1f77bcf86cd799439011abc')).toThrow(); // too long
|
|
86
|
+
expect(() => ghostIdSchema.parse('507f1f77bcf86cd79943901G')).toThrow(); // invalid char
|
|
87
|
+
expect(() => ghostIdSchema.parse('507F1F77BCF86CD799439011')).toThrow(); // uppercase
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('nqlFilterSchema', () => {
|
|
92
|
+
it('should accept valid NQL filter strings', () => {
|
|
93
|
+
expect(() => nqlFilterSchema.parse('status:published')).not.toThrow();
|
|
94
|
+
expect(() => nqlFilterSchema.parse('tag:news+featured:true')).not.toThrow();
|
|
95
|
+
expect(() => nqlFilterSchema.parse("author:'John Doe'")).not.toThrow();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should reject NQL strings with disallowed characters', () => {
|
|
99
|
+
expect(() => nqlFilterSchema.parse('status;DROP TABLE')).toThrow();
|
|
100
|
+
expect(() => nqlFilterSchema.parse('test&invalid')).toThrow();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should allow undefined/optional', () => {
|
|
104
|
+
expect(() => nqlFilterSchema.parse(undefined)).not.toThrow();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('limitSchema', () => {
|
|
109
|
+
it('should accept valid limits', () => {
|
|
110
|
+
expect(limitSchema.parse(1)).toBe(1);
|
|
111
|
+
expect(limitSchema.parse(50)).toBe(50);
|
|
112
|
+
expect(limitSchema.parse(100)).toBe(100);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should reject invalid limits', () => {
|
|
116
|
+
expect(() => limitSchema.parse(0)).toThrow();
|
|
117
|
+
expect(() => limitSchema.parse(101)).toThrow();
|
|
118
|
+
expect(() => limitSchema.parse(-1)).toThrow();
|
|
119
|
+
expect(() => limitSchema.parse(1.5)).toThrow();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should use default value', () => {
|
|
123
|
+
expect(limitSchema.parse(undefined)).toBe(15);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('pageSchema', () => {
|
|
128
|
+
it('should accept valid page numbers', () => {
|
|
129
|
+
expect(pageSchema.parse(1)).toBe(1);
|
|
130
|
+
expect(pageSchema.parse(100)).toBe(100);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should reject invalid page numbers', () => {
|
|
134
|
+
expect(() => pageSchema.parse(0)).toThrow();
|
|
135
|
+
expect(() => pageSchema.parse(-1)).toThrow();
|
|
136
|
+
expect(() => pageSchema.parse(1.5)).toThrow();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should use default value', () => {
|
|
140
|
+
expect(pageSchema.parse(undefined)).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('postStatusSchema', () => {
|
|
145
|
+
it('should accept valid statuses', () => {
|
|
146
|
+
expect(() => postStatusSchema.parse('draft')).not.toThrow();
|
|
147
|
+
expect(() => postStatusSchema.parse('published')).not.toThrow();
|
|
148
|
+
expect(() => postStatusSchema.parse('scheduled')).not.toThrow();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should reject invalid statuses', () => {
|
|
152
|
+
expect(() => postStatusSchema.parse('invalid')).toThrow();
|
|
153
|
+
expect(() => postStatusSchema.parse('DRAFT')).toThrow();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('visibilitySchema', () => {
|
|
158
|
+
it('should accept valid visibility values', () => {
|
|
159
|
+
expect(() => visibilitySchema.parse('public')).not.toThrow();
|
|
160
|
+
expect(() => visibilitySchema.parse('members')).not.toThrow();
|
|
161
|
+
expect(() => visibilitySchema.parse('paid')).not.toThrow();
|
|
162
|
+
expect(() => visibilitySchema.parse('tiers')).not.toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should reject invalid visibility values', () => {
|
|
166
|
+
expect(() => visibilitySchema.parse('private')).toThrow();
|
|
167
|
+
expect(() => visibilitySchema.parse('PUBLIC')).toThrow();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('htmlContentSchema', () => {
|
|
172
|
+
it('should accept non-empty HTML strings', () => {
|
|
173
|
+
expect(() => htmlContentSchema.parse('<p>Hello World</p>')).not.toThrow();
|
|
174
|
+
expect(() => htmlContentSchema.parse('Plain text')).not.toThrow();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should reject empty strings', () => {
|
|
178
|
+
expect(() => htmlContentSchema.parse('')).toThrow();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('titleSchema', () => {
|
|
183
|
+
it('should accept valid titles', () => {
|
|
184
|
+
expect(() => titleSchema.parse('My Blog Post')).not.toThrow();
|
|
185
|
+
expect(() => titleSchema.parse('A'.repeat(255))).not.toThrow();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should reject invalid titles', () => {
|
|
189
|
+
expect(() => titleSchema.parse('')).toThrow();
|
|
190
|
+
expect(() => titleSchema.parse('A'.repeat(256))).toThrow();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('excerptSchema', () => {
|
|
195
|
+
it('should accept valid excerpts', () => {
|
|
196
|
+
expect(() => excerptSchema.parse('A short description')).not.toThrow();
|
|
197
|
+
expect(() => excerptSchema.parse('A'.repeat(500))).not.toThrow();
|
|
198
|
+
expect(() => excerptSchema.parse(undefined)).not.toThrow();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should reject too long excerpts', () => {
|
|
202
|
+
expect(() => excerptSchema.parse('A'.repeat(501))).toThrow();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('metaTitleSchema', () => {
|
|
207
|
+
it('should accept valid meta titles', () => {
|
|
208
|
+
expect(() => metaTitleSchema.parse('SEO Title')).not.toThrow();
|
|
209
|
+
expect(() => metaTitleSchema.parse('A'.repeat(300))).not.toThrow();
|
|
210
|
+
expect(() => metaTitleSchema.parse(undefined)).not.toThrow();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should reject too long meta titles', () => {
|
|
214
|
+
expect(() => metaTitleSchema.parse('A'.repeat(301))).toThrow();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('metaDescriptionSchema', () => {
|
|
219
|
+
it('should accept valid meta descriptions', () => {
|
|
220
|
+
expect(() => metaDescriptionSchema.parse('SEO description')).not.toThrow();
|
|
221
|
+
expect(() => metaDescriptionSchema.parse('A'.repeat(500))).not.toThrow();
|
|
222
|
+
expect(() => metaDescriptionSchema.parse(undefined)).not.toThrow();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should reject too long meta descriptions', () => {
|
|
226
|
+
expect(() => metaDescriptionSchema.parse('A'.repeat(501))).toThrow();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('featuredSchema', () => {
|
|
231
|
+
it('should accept boolean values', () => {
|
|
232
|
+
expect(featuredSchema.parse(true)).toBe(true);
|
|
233
|
+
expect(featuredSchema.parse(false)).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should use default value', () => {
|
|
237
|
+
expect(featuredSchema.parse(undefined)).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('featureImageSchema', () => {
|
|
242
|
+
it('should accept valid image URLs', () => {
|
|
243
|
+
expect(() => featureImageSchema.parse('https://example.com/image.jpg')).not.toThrow();
|
|
244
|
+
expect(() => featureImageSchema.parse(undefined)).not.toThrow();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should reject invalid URLs', () => {
|
|
248
|
+
expect(() => featureImageSchema.parse('not-a-url')).toThrow();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('featureImageAltSchema', () => {
|
|
253
|
+
it('should accept valid alt text', () => {
|
|
254
|
+
expect(() => featureImageAltSchema.parse('Image description')).not.toThrow();
|
|
255
|
+
expect(() => featureImageAltSchema.parse('A'.repeat(125))).not.toThrow();
|
|
256
|
+
expect(() => featureImageAltSchema.parse(undefined)).not.toThrow();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should reject too long alt text', () => {
|
|
260
|
+
expect(() => featureImageAltSchema.parse('A'.repeat(126))).toThrow();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('tagNameSchema', () => {
|
|
265
|
+
it('should accept valid tag names', () => {
|
|
266
|
+
expect(() => tagNameSchema.parse('Technology')).not.toThrow();
|
|
267
|
+
expect(() => tagNameSchema.parse('A'.repeat(191))).not.toThrow();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should reject invalid tag names', () => {
|
|
271
|
+
expect(() => tagNameSchema.parse('')).toThrow();
|
|
272
|
+
expect(() => tagNameSchema.parse('A'.repeat(192))).toThrow();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createPostSchema,
|
|
4
|
+
updatePostSchema,
|
|
5
|
+
postQuerySchema,
|
|
6
|
+
postIdSchema,
|
|
7
|
+
postOutputSchema,
|
|
8
|
+
} from '../postSchemas.js';
|
|
9
|
+
|
|
10
|
+
describe('Post Schemas', () => {
|
|
11
|
+
describe('createPostSchema', () => {
|
|
12
|
+
it('should accept valid post creation data', () => {
|
|
13
|
+
const validPost = {
|
|
14
|
+
title: 'My Blog Post',
|
|
15
|
+
html: '<p>This is the content of the post.</p>',
|
|
16
|
+
status: 'draft',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
expect(() => createPostSchema.parse(validPost)).not.toThrow();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should accept minimal post creation data', () => {
|
|
23
|
+
const minimalPost = {
|
|
24
|
+
title: 'Title',
|
|
25
|
+
html: '<p>Content</p>',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const result = createPostSchema.parse(minimalPost);
|
|
29
|
+
expect(result.title).toBe('Title');
|
|
30
|
+
expect(result.status).toBe('draft'); // default
|
|
31
|
+
expect(result.visibility).toBe('public'); // default
|
|
32
|
+
expect(result.featured).toBe(false); // default
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should accept post with all fields', () => {
|
|
36
|
+
const fullPost = {
|
|
37
|
+
title: 'Complete Post',
|
|
38
|
+
html: '<p>Full content</p>',
|
|
39
|
+
slug: 'complete-post',
|
|
40
|
+
status: 'published',
|
|
41
|
+
visibility: 'members',
|
|
42
|
+
featured: true,
|
|
43
|
+
feature_image: 'https://example.com/image.jpg',
|
|
44
|
+
feature_image_alt: 'Image description',
|
|
45
|
+
feature_image_caption: 'Photo caption',
|
|
46
|
+
excerpt: 'Brief summary',
|
|
47
|
+
custom_excerpt: 'Custom summary',
|
|
48
|
+
meta_title: 'SEO Title',
|
|
49
|
+
meta_description: 'SEO Description',
|
|
50
|
+
tags: ['tech', 'news'],
|
|
51
|
+
authors: ['author@example.com'],
|
|
52
|
+
published_at: '2024-01-15T10:30:00.000Z',
|
|
53
|
+
canonical_url: 'https://example.com/original',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
expect(() => createPostSchema.parse(fullPost)).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should reject post without title', () => {
|
|
60
|
+
const invalidPost = {
|
|
61
|
+
html: '<p>Content</p>',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
expect(() => createPostSchema.parse(invalidPost)).toThrow();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should reject post without html', () => {
|
|
68
|
+
const invalidPost = {
|
|
69
|
+
title: 'Title',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
expect(() => createPostSchema.parse(invalidPost)).toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should reject post with invalid status', () => {
|
|
76
|
+
const invalidPost = {
|
|
77
|
+
title: 'Title',
|
|
78
|
+
html: '<p>Content</p>',
|
|
79
|
+
status: 'invalid',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
expect(() => createPostSchema.parse(invalidPost)).toThrow();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should reject post with too long title', () => {
|
|
86
|
+
const invalidPost = {
|
|
87
|
+
title: 'A'.repeat(256),
|
|
88
|
+
html: '<p>Content</p>',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
expect(() => createPostSchema.parse(invalidPost)).toThrow();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('updatePostSchema', () => {
|
|
96
|
+
it('should accept partial post updates', () => {
|
|
97
|
+
const update = {
|
|
98
|
+
title: 'Updated Title',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
expect(() => updatePostSchema.parse(update)).not.toThrow();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should accept empty update object', () => {
|
|
105
|
+
expect(() => updatePostSchema.parse({})).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('postQuerySchema', () => {
|
|
110
|
+
it('should accept valid query parameters', () => {
|
|
111
|
+
const query = {
|
|
112
|
+
limit: 20,
|
|
113
|
+
page: 2,
|
|
114
|
+
filter: 'status:published+featured:true',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
expect(() => postQuerySchema.parse(query)).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should accept query with include parameter', () => {
|
|
121
|
+
const query = {
|
|
122
|
+
include: 'tags,authors',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
expect(() => postQuerySchema.parse(query)).not.toThrow();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should accept empty query object', () => {
|
|
129
|
+
const result = postQuerySchema.parse({});
|
|
130
|
+
expect(result).toBeDefined();
|
|
131
|
+
// Note: optional fields with defaults don't apply when field is omitted
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('postIdSchema', () => {
|
|
136
|
+
it('should accept valid Ghost ID', () => {
|
|
137
|
+
const validId = {
|
|
138
|
+
id: '507f1f77bcf86cd799439011',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
expect(() => postIdSchema.parse(validId)).not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should reject invalid Ghost ID', () => {
|
|
145
|
+
const invalidId = {
|
|
146
|
+
id: 'invalid-id',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
expect(() => postIdSchema.parse(invalidId)).toThrow();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('postOutputSchema', () => {
|
|
154
|
+
it('should accept valid post output from Ghost API', () => {
|
|
155
|
+
const apiPost = {
|
|
156
|
+
id: '507f1f77bcf86cd799439011',
|
|
157
|
+
uuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
158
|
+
title: 'My Post',
|
|
159
|
+
slug: 'my-post',
|
|
160
|
+
html: '<p>Content</p>',
|
|
161
|
+
comment_id: null,
|
|
162
|
+
feature_image: 'https://example.com/image.jpg',
|
|
163
|
+
feature_image_alt: 'Alt text',
|
|
164
|
+
feature_image_caption: 'Caption',
|
|
165
|
+
featured: false,
|
|
166
|
+
status: 'published',
|
|
167
|
+
visibility: 'public',
|
|
168
|
+
created_at: '2024-01-15T10:30:00.000Z',
|
|
169
|
+
updated_at: '2024-01-15T10:30:00.000Z',
|
|
170
|
+
published_at: '2024-01-15T10:30:00.000Z',
|
|
171
|
+
custom_excerpt: 'Excerpt',
|
|
172
|
+
codeinjection_head: null,
|
|
173
|
+
codeinjection_foot: null,
|
|
174
|
+
custom_template: null,
|
|
175
|
+
canonical_url: null,
|
|
176
|
+
url: 'https://example.com/my-post',
|
|
177
|
+
excerpt: 'Auto excerpt',
|
|
178
|
+
reading_time: 5,
|
|
179
|
+
email_only: false,
|
|
180
|
+
og_image: null,
|
|
181
|
+
og_title: null,
|
|
182
|
+
og_description: null,
|
|
183
|
+
twitter_image: null,
|
|
184
|
+
twitter_title: null,
|
|
185
|
+
twitter_description: null,
|
|
186
|
+
meta_title: null,
|
|
187
|
+
meta_description: null,
|
|
188
|
+
email_subject: null,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
expect(() => postOutputSchema.parse(apiPost)).not.toThrow();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|