forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1import { assertEquals, assertStringIncludes } from "jsr:@std/assert";
2import { Project } from "ts-morph";
3import { generateClient } from "../src/client.ts";
4import type { Lexicon } from "../src/mod.ts";
5
6function createTestProject() {
7 return new Project({ useInMemoryFileSystem: true });
8}
9
10Deno.test("generateClient - creates nested client structure", () => {
11 const project = createTestProject();
12 const sourceFile = project.createSourceFile("test.ts", "");
13
14 const lexicons: Lexicon[] = [
15 {
16 id: "com.example.post",
17 definitions: {
18 main: {
19 type: "record",
20 record: {
21 type: "record",
22 properties: {
23 text: { type: "string" },
24 },
25 },
26 },
27 },
28 },
29 ];
30
31 generateClient(sourceFile, lexicons);
32 const result = sourceFile.getFullText();
33
34 // Should create main AtProtoClient class
35 assertStringIncludes(result, "export class AtProtoClient extends SlicesClient");
36
37 // Should create nested class structure
38 assertStringIncludes(result, "class ComClient");
39 assertStringIncludes(result, "class PostExampleComClient");
40
41 // Should have nested properties
42 assertStringIncludes(result, "readonly com: ComClient;");
43 assertStringIncludes(result, "readonly post: PostExampleComClient;");
44
45 // Should have OAuth client property
46 assertStringIncludes(result, "readonly oauth?: OAuthClient | AuthProvider;");
47});
48
49Deno.test("generateClient - creates CRUD methods for records", () => {
50 const project = createTestProject();
51 const sourceFile = project.createSourceFile("test.ts", "");
52
53 const lexicons: Lexicon[] = [
54 {
55 id: "app.bsky.feed.post",
56 definitions: {
57 main: {
58 type: "record",
59 record: {
60 type: "record",
61 properties: {
62 text: { type: "string" },
63 createdAt: { type: "string" },
64 },
65 },
66 },
67 },
68 },
69 ];
70
71 generateClient(sourceFile, lexicons);
72 const result = sourceFile.getFullText();
73
74 // Should create CRUD methods
75 assertStringIncludes(result, "async getRecords(");
76 assertStringIncludes(result, "async getRecord(");
77 assertStringIncludes(result, "async createRecord(");
78 assertStringIncludes(result, "async updateRecord(");
79 assertStringIncludes(result, "async deleteRecord(");
80 assertStringIncludes(result, "async countRecords(");
81
82 // Should have proper return types
83 assertStringIncludes(result, "Promise<GetRecordsResponse<AppBskyFeedPost>>");
84 assertStringIncludes(result, "Promise<RecordResponse<AppBskyFeedPost>>");
85 assertStringIncludes(result, 'return await this.client.getRecords(\'app.bsky.feed.post\', params);');
86});
87
88Deno.test("generateClient - creates constructors with proper inheritance", () => {
89 const project = createTestProject();
90 const sourceFile = project.createSourceFile("test.ts", "");
91
92 const lexicons: Lexicon[] = [
93 {
94 id: "com.example.test",
95 definitions: {
96 main: {
97 type: "record",
98 record: {
99 type: "record",
100 properties: { name: { type: "string" } },
101 },
102 },
103 },
104 },
105 ];
106
107 generateClient(sourceFile, lexicons);
108 const result = sourceFile.getFullText();
109
110 // Main client constructor
111 assertStringIncludes(result, "constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient | AuthProvider)");
112 assertStringIncludes(result, "super(baseUrl, sliceUri, oauthClient);");
113
114 // Nested class constructors
115 assertStringIncludes(result, "constructor(client: SlicesClient)");
116 assertStringIncludes(result, "this.client = client;");
117});
118
119Deno.test("generateClient - creates client for network.slices lexicons", () => {
120 const project = createTestProject();
121 const sourceFile = project.createSourceFile("test.ts", "");
122
123 const lexicons: Lexicon[] = [
124 {
125 id: "network.slices.slice",
126 definitions: {
127 main: {
128 type: "record",
129 record: {
130 type: "record",
131 properties: { name: { type: "string" } },
132 },
133 },
134 },
135 },
136 ];
137
138 generateClient(sourceFile, lexicons);
139 const result = sourceFile.getFullText();
140
141 // Should create proper client structure for network.slices
142 assertStringIncludes(result, "export class AtProtoClient extends SlicesClient");
143 assertStringIncludes(result, "class NetworkClient");
144 assertStringIncludes(result, "class SlicesNetworkClient");
145 assertStringIncludes(result, "readonly network: NetworkClient;");
146 assertStringIncludes(result, "readonly slices: SlicesNetworkClient;");
147
148 // Should include standard CRUD methods
149 assertStringIncludes(result, "async getRecords(");
150 assertStringIncludes(result, "async createRecord(");
151});
152
153Deno.test("generateClient - sanitizes hyphenated NSIDs", () => {
154 const project = createTestProject();
155 const sourceFile = project.createSourceFile("test.ts", "");
156
157 const lexicons: Lexicon[] = [
158 {
159 id: "network.slices.cyber-meteor-1637.actor.profile",
160 definitions: {
161 main: {
162 type: "record",
163 record: {
164 type: "record",
165 properties: {
166 displayName: { type: "string" },
167 description: { type: "string" },
168 },
169 },
170 },
171 },
172 },
173 ];
174
175 generateClient(sourceFile, lexicons);
176 const result = sourceFile.getFullText();
177
178 // Should sanitize hyphenated names to camelCase
179 assertStringIncludes(result, "readonly cyberMeteor1637: CyberMeteor1637SlicesNetworkClient;");
180 assertStringIncludes(result, "class CyberMeteor1637SlicesNetworkClient");
181 assertStringIncludes(result, "class ActorCyberMeteor1637SlicesNetworkClient");
182 assertStringIncludes(result, "class ProfileActorCyberMeteor1637SlicesNetworkClient");
183
184 // Constructor should use sanitized property names
185 assertStringIncludes(result, "this.cyberMeteor1637 = new CyberMeteor1637SlicesNetworkClient(");
186
187 // But should preserve original NSID in API calls
188 assertStringIncludes(result, "'network.slices.cyber-meteor-1637.actor.profile'");
189});
190
191Deno.test("generateClient - handles mixed lexicon types", () => {
192 const project = createTestProject();
193 const sourceFile = project.createSourceFile("test.ts", "");
194
195 const lexicons: Lexicon[] = [
196 {
197 id: "app.bsky.feed.post",
198 definitions: {
199 main: {
200 type: "record",
201 record: {
202 type: "record",
203 properties: { text: { type: "string" } },
204 },
205 },
206 },
207 },
208 {
209 id: "network.slices.slice",
210 definitions: {
211 main: {
212 type: "record",
213 record: {
214 type: "record",
215 properties: { name: { type: "string" } },
216 },
217 },
218 },
219 },
220 ];
221
222 generateClient(sourceFile, lexicons);
223 const result = sourceFile.getFullText();
224
225 // Should include both app.bsky and network.slices clients
226 assertStringIncludes(result, "class AppClient");
227 assertStringIncludes(result, "class NetworkClient");
228 assertStringIncludes(result, "readonly app: AppClient;");
229 assertStringIncludes(result, "readonly network: NetworkClient;");
230});
231
232Deno.test("generateClient - handles deep nesting correctly", () => {
233 const project = createTestProject();
234 const sourceFile = project.createSourceFile("test.ts", "");
235
236 const lexicons: Lexicon[] = [
237 {
238 id: "app.bsky.feed.post",
239 definitions: {
240 main: {
241 type: "record",
242 record: {
243 type: "record",
244 properties: { text: { type: "string" } },
245 },
246 },
247 },
248 },
249 {
250 id: "app.bsky.actor.profile",
251 definitions: {
252 main: {
253 type: "record",
254 record: {
255 type: "record",
256 properties: { displayName: { type: "string" } },
257 },
258 },
259 },
260 },
261 ];
262
263 generateClient(sourceFile, lexicons);
264 const result = sourceFile.getFullText();
265
266 // Should create proper nesting: app.bsky.feed and app.bsky.actor
267 assertStringIncludes(result, "class AppClient");
268 assertStringIncludes(result, "class BskyAppClient");
269 assertStringIncludes(result, "class FeedBskyAppClient");
270 assertStringIncludes(result, "class ActorBskyAppClient");
271
272 // Should have proper property chains
273 assertStringIncludes(result, "readonly app: AppClient;");
274 assertStringIncludes(result, "readonly bsky: BskyAppClient;");
275 assertStringIncludes(result, "readonly feed: FeedBskyAppClient;");
276 assertStringIncludes(result, "readonly actor: ActorBskyAppClient;");
277});
278
279Deno.test("generateClient - creates no client when no record lexicons", () => {
280 const project = createTestProject();
281 const sourceFile = project.createSourceFile("test.ts", "");
282
283 const lexicons: Lexicon[] = [
284 {
285 id: "com.example.defs",
286 definitions: {
287 main: {
288 type: "object",
289 properties: { value: { type: "string" } },
290 },
291 },
292 },
293 ];
294
295 generateClient(sourceFile, lexicons);
296 const result = sourceFile.getFullText();
297
298 // Should not create any client classes for non-record lexicons
299 assertEquals(result.includes("export class AtProtoClient"), false);
300 assertEquals(result.includes("class"), false);
301});