Highly ambitious ATProtocol AppView service and sdks
at main 301 lines 9.2 kB view raw
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});