Skip to content

Commit 9e2dd47

Browse files
committed
test(auth): rewrite loopback client_id tests for full takeover behavior
7 tests covering: bare localhost, trailing slash, 127.0.0.1 rewrite, localhost:3000 rewrite, existing query params rewrite, non-loopback passthrough, and scope always being atproto transition:generic.
1 parent 86b9350 commit 9e2dd47

1 file changed

Lines changed: 243 additions & 0 deletions

File tree

packages/sdk-core/tests/auth/OAuthClient.test.ts

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,247 @@ describe("OAuthClient", () => {
291291
expect(warnLogs.length).toBe(0);
292292
});
293293
});
294+
295+
describe("loopback client_id auto-generation", () => {
296+
it("should auto-generate for http://localhost", async () => {
297+
const { MockLogger } = await import("../utils/mocks.js");
298+
const logger = new MockLogger();
299+
const baseConfig = await createTestConfigAsync({ logger });
300+
const configWithLoopback = await createTestConfigAsync({
301+
logger,
302+
oauth: {
303+
...baseConfig.oauth,
304+
clientId: "http://localhost",
305+
scope: "atproto",
306+
redirectUri: "http://127.0.0.1:3000/callback",
307+
developmentMode: true,
308+
},
309+
});
310+
311+
const client = new OAuthClient(configWithLoopback);
312+
313+
// Trigger async initialization to run buildClientMetadata()
314+
try {
315+
await client.authorize("test.bsky.social");
316+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
317+
} catch (error) {
318+
// Expected - underlying client may reject loopback URLs
319+
}
320+
321+
// Verify info log contains "Development mode"
322+
const infoLogs = logger.logs.filter((log) => log.level === "info");
323+
const devModeLog = infoLogs.find((log) => log.message.includes("Development mode"));
324+
expect(devModeLog).toBeDefined();
325+
});
326+
327+
it("should auto-generate for http://localhost/ (trailing slash)", async () => {
328+
const { MockLogger } = await import("../utils/mocks.js");
329+
const logger = new MockLogger();
330+
const baseConfig = await createTestConfigAsync({ logger });
331+
const configWithLoopback = await createTestConfigAsync({
332+
logger,
333+
oauth: {
334+
...baseConfig.oauth,
335+
clientId: "http://localhost/",
336+
scope: "atproto",
337+
redirectUri: "http://127.0.0.1:3000/callback",
338+
developmentMode: true,
339+
},
340+
});
341+
342+
const client = new OAuthClient(configWithLoopback);
343+
344+
// Trigger async initialization to run buildClientMetadata()
345+
try {
346+
await client.authorize("test.bsky.social");
347+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
348+
} catch (error) {
349+
// Expected - underlying client may reject loopback URLs
350+
}
351+
352+
// Verify info log emitted
353+
const infoLogs = logger.logs.filter((log) => log.level === "info");
354+
const devModeLog = infoLogs.find((log) => log.message.includes("Development mode"));
355+
expect(devModeLog).toBeDefined();
356+
357+
// Verify NO rewrite warning (http://localhost/ is acceptable)
358+
const warnLogs = logger.logs.filter((log) => log.level === "warn");
359+
const rewriteWarning = warnLogs.find((log) => log.message.includes("Rewriting client_id"));
360+
expect(rewriteWarning).toBeUndefined();
361+
});
362+
363+
it("should auto-generate for http://127.0.0.1", async () => {
364+
const { MockLogger } = await import("../utils/mocks.js");
365+
const logger = new MockLogger();
366+
const baseConfig = await createTestConfigAsync({ logger });
367+
const configWithLoopback = await createTestConfigAsync({
368+
logger,
369+
oauth: {
370+
...baseConfig.oauth,
371+
clientId: "http://127.0.0.1",
372+
scope: "atproto",
373+
redirectUri: "http://127.0.0.1:3000/callback",
374+
developmentMode: true,
375+
},
376+
});
377+
378+
const client = new OAuthClient(configWithLoopback);
379+
380+
// Trigger async initialization to run buildClientMetadata()
381+
try {
382+
await client.authorize("test.bsky.social");
383+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
384+
} catch (error) {
385+
// Expected - underlying client may reject loopback URLs
386+
}
387+
388+
// Verify info log contains "Development mode"
389+
const infoLogs = logger.logs.filter((log) => log.level === "info");
390+
const devModeLog = infoLogs.find((log) => log.message.includes("Development mode"));
391+
expect(devModeLog).toBeDefined();
392+
393+
// Verify rewrite warning mentioning "Rewriting client_id"
394+
const warnLogs = logger.logs.filter((log) => log.level === "warn");
395+
const rewriteWarning = warnLogs.find((log) => log.message.includes("Rewriting client_id"));
396+
expect(rewriteWarning).toBeDefined();
397+
});
398+
399+
it("should auto-generate for http://localhost:3000 (with port)", async () => {
400+
const { MockLogger } = await import("../utils/mocks.js");
401+
const logger = new MockLogger();
402+
const baseConfig = await createTestConfigAsync({ logger });
403+
const configWithPort = await createTestConfigAsync({
404+
logger,
405+
oauth: {
406+
...baseConfig.oauth,
407+
clientId: "http://localhost:3000",
408+
scope: "atproto",
409+
redirectUri: "http://127.0.0.1:3000/callback",
410+
developmentMode: true,
411+
},
412+
});
413+
414+
const client = new OAuthClient(configWithPort);
415+
416+
// Trigger async initialization to run buildClientMetadata()
417+
try {
418+
await client.authorize("test.bsky.social");
419+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
420+
} catch (error) {
421+
// Expected - underlying client may reject loopback URLs
422+
}
423+
424+
// Verify info log contains "Development mode"
425+
const infoLogs = logger.logs.filter((log) => log.level === "info");
426+
const devModeLog = infoLogs.find((log) => log.message.includes("Development mode"));
427+
expect(devModeLog).toBeDefined();
428+
429+
// Verify rewrite warning
430+
const warnLogs = logger.logs.filter((log) => log.level === "warn");
431+
const rewriteWarning = warnLogs.find((log) => log.message.includes("Rewriting client_id"));
432+
expect(rewriteWarning).toBeDefined();
433+
});
434+
435+
it("should auto-generate for http://localhost?scope=custom (with existing query params)", async () => {
436+
const { MockLogger } = await import("../utils/mocks.js");
437+
const logger = new MockLogger();
438+
const baseConfig = await createTestConfigAsync({ logger });
439+
const configWithQueryParams = await createTestConfigAsync({
440+
logger,
441+
oauth: {
442+
...baseConfig.oauth,
443+
clientId: "http://localhost?scope=custom&redirect_uri=http://127.0.0.1:3000/cb",
444+
scope: "atproto",
445+
redirectUri: "http://127.0.0.1:3000/callback",
446+
developmentMode: true,
447+
},
448+
});
449+
450+
const client = new OAuthClient(configWithQueryParams);
451+
452+
// Trigger async initialization to run buildClientMetadata()
453+
try {
454+
await client.authorize("test.bsky.social");
455+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
456+
} catch (error) {
457+
// Expected - underlying client may reject loopback URLs
458+
}
459+
460+
// Verify info log contains "Development mode" (we take over regardless)
461+
const infoLogs = logger.logs.filter((log) => log.level === "info");
462+
const devModeLog = infoLogs.find((log) => log.message.includes("Development mode"));
463+
expect(devModeLog).toBeDefined();
464+
465+
// Verify rewrite warning (we take over regardless of existing query params)
466+
const warnLogs = logger.logs.filter((log) => log.level === "warn");
467+
const rewriteWarning = warnLogs.find((log) => log.message.includes("Rewriting client_id"));
468+
expect(rewriteWarning).toBeDefined();
469+
});
470+
471+
it("should not touch non-loopback client_id", async () => {
472+
const { MockLogger } = await import("../utils/mocks.js");
473+
const logger = new MockLogger();
474+
const baseConfig = await createTestConfigAsync({ logger });
475+
const configWithNonLocalhost = await createTestConfigAsync({
476+
logger,
477+
oauth: {
478+
...baseConfig.oauth,
479+
clientId: "https://example.com/client-metadata.json",
480+
scope: "atproto",
481+
redirectUri: "https://example.com/callback",
482+
},
483+
});
484+
485+
const client = new OAuthClient(configWithNonLocalhost);
486+
487+
// Trigger async initialization to run buildClientMetadata()
488+
try {
489+
await client.authorize("test.bsky.social");
490+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
491+
} catch (error) {
492+
// Expected - network error or other issues
493+
}
494+
495+
// Verify NO info log containing "Development mode"
496+
const infoLogs = logger.logs.filter((log) => log.level === "info");
497+
const devModeLog = infoLogs.find((log) => log.message.includes("Development mode"));
498+
expect(devModeLog).toBeUndefined();
499+
});
500+
501+
it("should always use atproto transition:generic scope", async () => {
502+
const { MockLogger } = await import("../utils/mocks.js");
503+
const logger = new MockLogger();
504+
const baseConfig = await createTestConfigAsync({ logger });
505+
const configWithLoopback = await createTestConfigAsync({
506+
logger,
507+
oauth: {
508+
...baseConfig.oauth,
509+
clientId: "http://localhost",
510+
scope: "atproto",
511+
redirectUri: "http://127.0.0.1:3000/callback",
512+
developmentMode: true,
513+
},
514+
});
515+
516+
const client = new OAuthClient(configWithLoopback);
517+
518+
// Trigger async initialization to run buildClientMetadata()
519+
try {
520+
await client.authorize("test.bsky.social");
521+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
522+
} catch (error) {
523+
// Expected - underlying client may reject loopback URLs
524+
}
525+
526+
// Verify info log mentions "atproto transition:generic"
527+
const infoLogs = logger.logs.filter((log) => log.level === "info");
528+
const scopeLog = infoLogs.find((log) => log.message.includes("atproto transition:generic"));
529+
expect(scopeLog).toBeDefined();
530+
531+
// Verify NO scope override warning (we discussed this, it's dev mode, nobody cares)
532+
const warnLogs = logger.logs.filter((log) => log.level === "warn");
533+
const scopeOverrideWarning = warnLogs.find((log) => log.message.includes("overriding configured scope"));
534+
expect(scopeOverrideWarning).toBeUndefined();
535+
});
536+
});
294537
});

0 commit comments

Comments
 (0)