A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import { describe, expect, it } from 'vitest';
2import {
3 getClientMode,
4 clientMetadataUrl,
5 normalizeHandle,
6 isValidAccountInput,
7 isValidHandleOrDid,
8 redirectUriForLocation,
9 OAUTH_REDIRECT_PATHS,
10 OAUTH_SCOPE,
11} from './config';
12
13describe( 'getClientMode', () => {
14 it( 'treats loopback hosts as dev (loopback) clients', () => {
15 expect( getClientMode( 'localhost' ) ).toBe( 'loopback' );
16 expect( getClientMode( '127.0.0.1' ) ).toBe( 'loopback' );
17 expect( getClientMode( '[::1]' ) ).toBe( 'loopback' );
18 } );
19
20 it( 'treats real hosts as hosted clients', () => {
21 expect( getClientMode( 'skypress.blog' ) ).toBe( 'hosted' );
22 } );
23} );
24
25describe( 'clientMetadataUrl', () => {
26 it( 'points at /client-metadata.json on the app origin', () => {
27 expect( clientMetadataUrl( 'https://skypress.blog' ) ).toBe(
28 'https://skypress.blog/client-metadata.json'
29 );
30 } );
31} );
32
33describe( 'OAUTH_REDIRECT_PATHS', () => {
34 it( 'lists every route that mounts the OAuth island, /editor/ first (the default)', () => {
35 // Both editor surfaces must be registered redirect targets, or atproto rejects
36 // the round-trip; /editor/ stays first so it is the safe fallback for any other
37 // page that ever triggers sign-in.
38 expect( OAUTH_REDIRECT_PATHS ).toEqual( [ '/editor/', '/write/' ] );
39 } );
40} );
41
42describe( 'redirectUriForLocation', () => {
43 it( 'returns the originating page so the OAuth round-trip comes back to it', () => {
44 // The bug: signing in from /write returned the writer to /editor (the only
45 // registered redirect), orphaning the writing-first draft + publish intent.
46 expect( redirectUriForLocation( 'https://skypress.blog', '/write/' ) ).toBe(
47 'https://skypress.blog/write/'
48 );
49 expect( redirectUriForLocation( 'https://skypress.blog', '/editor/' ) ).toBe(
50 'https://skypress.blog/editor/'
51 );
52 } );
53
54 it( 'normalises a missing trailing slash to the registered (slashed) path', () => {
55 expect( redirectUriForLocation( 'https://skypress.blog', '/write' ) ).toBe(
56 'https://skypress.blog/write/'
57 );
58 } );
59
60 it( 'falls back to /editor/ for any non-editor path', () => {
61 expect( redirectUriForLocation( 'https://skypress.blog', '/' ) ).toBe(
62 'https://skypress.blog/editor/'
63 );
64 expect( redirectUriForLocation( 'https://skypress.blog', '/dashboard/' ) ).toBe(
65 'https://skypress.blog/editor/'
66 );
67 } );
68
69 it( 'strips a trailing slash off the origin before joining', () => {
70 expect( redirectUriForLocation( 'https://skypress.blog/', '/write/' ) ).toBe(
71 'https://skypress.blog/write/'
72 );
73 } );
74} );
75
76describe( 'normalizeHandle', () => {
77 it( 'strips a leading @, trims, and lowercases', () => {
78 expect( normalizeHandle( ' @Alice.BSKY.social ' ) ).toBe( 'alice.bsky.social' );
79 expect( normalizeHandle( 'bob.com' ) ).toBe( 'bob.com' );
80 } );
81} );
82
83describe( 'isValidAccountInput', () => {
84 it( 'accepts handles, DIDs and PDS URLs', () => {
85 expect( isValidAccountInput( 'alice.bsky.social' ) ).toBe( true );
86 expect( isValidAccountInput( '@alice.bsky.social' ) ).toBe( true );
87 expect( isValidAccountInput( 'did:plc:abc123' ) ).toBe( true );
88 expect( isValidAccountInput( 'https://pds.example.com' ) ).toBe( true );
89 } );
90
91 it( 'rejects empty input and bare words without a domain', () => {
92 expect( isValidAccountInput( '' ) ).toBe( false );
93 expect( isValidAccountInput( ' ' ) ).toBe( false );
94 expect( isValidAccountInput( 'notahandle' ) ).toBe( false );
95 } );
96} );
97
98describe( 'isValidHandleOrDid', () => {
99 it( 'accepts handles and DIDs', () => {
100 expect( isValidHandleOrDid( 'alice.bsky.social' ) ).toBe( true );
101 expect( isValidHandleOrDid( '@alice.bsky.social' ) ).toBe( true );
102 expect( isValidHandleOrDid( 'did:plc:abc123' ) ).toBe( true );
103 expect( isValidHandleOrDid( 'did:web:example.com' ) ).toBe( true );
104 } );
105
106 it( 'rejects URLs and anything carrying a path, port, query or scheme', () => {
107 // These would otherwise be used directly as a resolver fetch host (SSRF/proxy).
108 expect( isValidHandleOrDid( 'https://pds.example.com' ) ).toBe( false );
109 expect( isValidHandleOrDid( 'evil.com/.well-known/atproto-did' ) ).toBe( false );
110 expect( isValidHandleOrDid( 'evil.com?x=y' ) ).toBe( false );
111 expect( isValidHandleOrDid( 'evil.com:8080' ) ).toBe( false );
112 expect( isValidHandleOrDid( 'user@evil.com' ) ).toBe( false );
113 } );
114
115 it( 'rejects empty input and bare words without a domain', () => {
116 expect( isValidHandleOrDid( '' ) ).toBe( false );
117 expect( isValidHandleOrDid( ' ' ) ).toBe( false );
118 expect( isValidHandleOrDid( 'notahandle' ) ).toBe( false );
119 } );
120} );
121
122describe( 'OAUTH_SCOPE', () => {
123 it( 'requests atproto + transitional generic access for SP1', () => {
124 expect( OAUTH_SCOPE ).toBe( 'atproto transition:generic' );
125 } );
126} );