A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2import { act, createElement, useContext, type ReactNode } from 'react';
3import { createRoot } from 'react-dom/client';
4import { AuthContext, AuthProvider, type AuthContextValue } from './AuthProvider';
5
6( globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true;
7
8// A signed-out browser OAuth client whose `signIn` we capture. `init()` resolves with no
9// session so the provider settles on 'signed-out' without touching the PDS/profile code.
10const oauth = vi.hoisted( () => ( {
11 signIn: vi.fn( async () => {} ),
12} ) );
13vi.mock( './oauth', () => ( {
14 createOAuthClient: async () => ( {
15 init: async () => null,
16 signIn: oauth.signIn,
17 revoke: async () => {},
18 } ),
19} ) );
20
21/** Render the provider and hand back the live context value (incl. `signIn`). */
22async function mountProvider() {
23 const captured: { value: AuthContextValue | null } = { value: null };
24 function Capture( { sink }: { sink: { value: AuthContextValue | null } } ): ReactNode {
25 sink.value = useContext( AuthContext );
26 return null;
27 }
28 const container = document.createElement( 'div' );
29 const root = createRoot( container );
30 await act( async () => {
31 root.render( createElement( AuthProvider, null, createElement( Capture, { sink: captured } ) ) );
32 } );
33 return captured;
34}
35
36function setLocation( origin: string, pathname: string, hostname: string ) {
37 vi.stubGlobal( 'location', { origin, pathname, hostname } );
38}
39
40describe( 'AuthProvider signIn redirect target', () => {
41 beforeEach( () => {
42 oauth.signIn.mockClear();
43 } );
44 afterEach( () => {
45 vi.unstubAllGlobals();
46 } );
47
48 it( 'hosted: returns to the page sign-in started from (/write/, not /editor/)', async () => {
49 setLocation( 'https://skypress.blog', '/write/', 'skypress.blog' );
50 const ctx = await mountProvider();
51 await act( async () => {
52 await ctx.value!.signIn( 'alice.bsky.social' );
53 } );
54 expect( oauth.signIn ).toHaveBeenCalledWith( 'alice.bsky.social', {
55 redirect_uri: 'https://skypress.blog/write/',
56 } );
57 } );
58
59 it( 'loopback (dev): passes no redirect_uri — the per-page loopback client already round-trips', async () => {
60 setLocation( 'http://127.0.0.1:4321', '/write', '127.0.0.1' );
61 const ctx = await mountProvider();
62 await act( async () => {
63 await ctx.value!.signIn( 'alice.bsky.social' );
64 } );
65 // `undefined` options is equivalent to omitting them — the loopback metadata's lone
66 // redirect URI is the current page, so atproto comes back here on its own.
67 expect( oauth.signIn ).toHaveBeenCalledWith( 'alice.bsky.social', undefined );
68 } );
69} );