feat(release): stage GitWorkshop and next node updates
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const providerSource = readFileSync(
|
||||
resolve(process.cwd(), 'public/nostr-provider.js'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
type ProviderWindow = Window & {
|
||||
__archipelagoNostr?: boolean
|
||||
ArchipelagoSurface?: {
|
||||
expectPageTransition: () => void
|
||||
}
|
||||
nostr?: { getPublicKey: () => Promise<string> }
|
||||
archipelagoNostr?: {
|
||||
selectIdentity: () => Promise<unknown>
|
||||
getSelectedIdentity: () => { nostr_pubkey: string } | null
|
||||
onIdentitySelected: (
|
||||
callback: (identity: { nostr_pubkey: string }) => void,
|
||||
) => () => void
|
||||
}
|
||||
}
|
||||
|
||||
describe('nostr-provider identity selection', () => {
|
||||
let providerWindow: ProviderWindow
|
||||
|
||||
beforeEach(() => {
|
||||
providerWindow = window as ProviderWindow
|
||||
delete providerWindow.__archipelagoNostr
|
||||
delete providerWindow.nostr
|
||||
delete providerWindow.archipelagoNostr
|
||||
delete providerWindow.ArchipelagoSurface
|
||||
document.documentElement.innerHTML = '<head><title>IndeedHub</title></head><body></body>'
|
||||
window.history.replaceState({}, '', '/app/indeedhub/')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
Reflect.deleteProperty(document, 'readyState')
|
||||
delete providerWindow.__archipelagoNostr
|
||||
delete providerWindow.nostr
|
||||
delete providerWindow.archipelagoNostr
|
||||
delete providerWindow.ArchipelagoSurface
|
||||
})
|
||||
|
||||
function loadProvider(userActivated: boolean) {
|
||||
Object.defineProperty(navigator, 'userActivation', {
|
||||
configurable: true,
|
||||
value: { isActive: userActivated },
|
||||
})
|
||||
window.eval(providerSource)
|
||||
window.dispatchEvent(new Event('load'))
|
||||
const frame = document.querySelector<HTMLIFrameElement>('#archipelago-nostr-signer')!
|
||||
const postMessage = vi.spyOn(frame.contentWindow!, 'postMessage')
|
||||
const signerOriginUrl = new URL(window.location.href)
|
||||
signerOriginUrl.port = ''
|
||||
const signerOrigin = signerOriginUrl.origin
|
||||
const ready = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-ready' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(ready, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(ready)
|
||||
postMessage.mockClear()
|
||||
return { frame, postMessage, signerOrigin }
|
||||
}
|
||||
|
||||
it('reopens the chooser for a user-triggered NIP-07 login', async () => {
|
||||
const { frame, postMessage, signerOrigin } = loadProvider(true)
|
||||
const publicKey = providerWindow.nostr!.getPublicKey()
|
||||
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'archipelago:signer-select-identity', force: true }),
|
||||
signerOrigin,
|
||||
)
|
||||
|
||||
const selected = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'abc123' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(selected, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(selected)
|
||||
await Promise.resolve()
|
||||
|
||||
await expect(publicKey).resolves.toBe('abc123')
|
||||
expect(postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'nostr-request', method: 'getPublicKey' }),
|
||||
signerOrigin,
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the remembered identity for background account restoration', () => {
|
||||
const { postMessage, signerOrigin } = loadProvider(false)
|
||||
void providerWindow.nostr!.getPublicKey()
|
||||
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'nostr-request', method: 'getPublicKey' }),
|
||||
signerOrigin,
|
||||
)
|
||||
expect(postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'archipelago:signer-select-identity' }),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps an eager picker choice for the app login that follows', async () => {
|
||||
const { frame, postMessage, signerOrigin } = loadProvider(false)
|
||||
const selected = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'fast-choice' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(selected, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(selected)
|
||||
postMessage.mockClear()
|
||||
|
||||
await expect(providerWindow.nostr!.getPublicKey()).resolves.toBe('fast-choice')
|
||||
expect(postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'nostr-request', method: 'getPublicKey' }),
|
||||
signerOrigin,
|
||||
)
|
||||
})
|
||||
|
||||
it('parks the hidden broker off-screen and reuses it for the next request', async () => {
|
||||
const surface = {
|
||||
expectPageTransition: vi.fn(),
|
||||
}
|
||||
providerWindow.ArchipelagoSurface = surface
|
||||
const { frame, postMessage, signerOrigin } = loadProvider(false)
|
||||
|
||||
const show = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-show' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(show, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(show)
|
||||
expect(frame.style.display).toBe('block')
|
||||
|
||||
const hide = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-hide' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(hide, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(hide)
|
||||
|
||||
expect(frame.style.display).toBe('block')
|
||||
expect(frame.style.width).toBe('1px')
|
||||
expect(frame.style.height).toBe('1px')
|
||||
expect(frame.style.opacity).toBe('0')
|
||||
expect(frame.style.pointerEvents).toBe('none')
|
||||
expect(frame.style.transform).toContain('-10000px')
|
||||
expect(document.querySelector('#archipelago-nostr-signer')).toBe(frame)
|
||||
|
||||
const publicKey = providerWindow.nostr!.getPublicKey()
|
||||
const replacement = document.querySelector<HTMLIFrameElement>('#archipelago-nostr-signer')!
|
||||
expect(replacement).toBe(frame)
|
||||
const ready = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-ready' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(ready, 'source', { value: replacement.contentWindow })
|
||||
window.dispatchEvent(ready)
|
||||
|
||||
const request = postMessage.mock.calls
|
||||
.map(call => call[0] as { type: string; id?: number })
|
||||
.find(message => message.type === 'nostr-request')!
|
||||
expect(request).toBeDefined()
|
||||
const response = new MessageEvent('message', {
|
||||
data: { type: 'nostr-response', id: request.id, result: 'recreated-key' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(response, 'source', { value: replacement.contentWindow })
|
||||
window.dispatchEvent(response)
|
||||
await expect(publicKey).resolves.toBe('recreated-key')
|
||||
})
|
||||
|
||||
it('delivers an eager identity to an app listener that mounts afterward', () => {
|
||||
const { frame, signerOrigin } = loadProvider(false)
|
||||
const selected = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'late-listener' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(selected, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(selected)
|
||||
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = providerWindow.archipelagoNostr!.onIdentitySelected(listener)
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(listener).toHaveBeenCalledWith({ nostr_pubkey: 'late-listener' })
|
||||
expect(providerWindow.archipelagoNostr!.getSelectedIdentity())
|
||||
.toEqual({ nostr_pubkey: 'late-listener' })
|
||||
|
||||
unsubscribe()
|
||||
const changed = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'after-unsubscribe' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(changed, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(changed)
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('turns an automatic IndeeHub identity into a NIP-98 signing request', async () => {
|
||||
vi.useFakeTimers()
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const { postMessage, signerOrigin } = loadProvider(false)
|
||||
|
||||
const identity = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:identity', nostr_pubkey: 'indeedhub-key' },
|
||||
origin: window.location.origin,
|
||||
})
|
||||
Object.defineProperty(identity, 'source', { value: window })
|
||||
window.dispatchEvent(identity)
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${window.location.origin}/api/nostr-auth/health`,
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
)
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'nostr-request',
|
||||
method: 'signEvent',
|
||||
params: { event: expect.objectContaining({ kind: 27235, pubkey: 'indeedhub-key' }) },
|
||||
}),
|
||||
signerOrigin,
|
||||
)
|
||||
})
|
||||
|
||||
it('waits for the signer success surface to hide before reloading after NIP-98', async () => {
|
||||
vi.useFakeTimers()
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce({ ok: true })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ accessToken: 'real-token', refreshToken: 'refresh' }),
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const raf = vi.spyOn(window, 'requestAnimationFrame').mockImplementation(() => 1)
|
||||
const { frame, postMessage, signerOrigin } = loadProvider(false)
|
||||
|
||||
const identity = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:identity', nostr_pubkey: 'indeedhub-key' },
|
||||
origin: window.location.origin,
|
||||
})
|
||||
Object.defineProperty(identity, 'source', { value: window })
|
||||
window.dispatchEvent(identity)
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
|
||||
const signRequest = postMessage.mock.calls
|
||||
.map(call => call[0] as { type: string; id?: number })
|
||||
.find(message => message.type === 'nostr-request' && message.id != null)!
|
||||
const show = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-show' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(show, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(show)
|
||||
|
||||
const signed = new MessageEvent('message', {
|
||||
data: { type: 'nostr-response', id: signRequest.id, result: { id: 'signed-event' } },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(signed, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(signed)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(sessionStorage.getItem('nostr_token')).toBe('real-token')
|
||||
expect(raf).not.toHaveBeenCalled()
|
||||
|
||||
const hide = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-hide' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(hide, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(hide)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(raf).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('queues a request until signer-init when the signer iframe wins the load race', () => {
|
||||
Object.defineProperty(navigator, 'userActivation', {
|
||||
configurable: true,
|
||||
value: { isActive: false },
|
||||
})
|
||||
Object.defineProperty(document, 'readyState', {
|
||||
configurable: true,
|
||||
value: 'loading',
|
||||
})
|
||||
window.eval(providerSource)
|
||||
const frame = document.querySelector<HTMLIFrameElement>('#archipelago-nostr-signer')!
|
||||
const postMessage = vi.spyOn(frame.contentWindow!, 'postMessage')
|
||||
const signerOriginUrl = new URL(window.location.href)
|
||||
signerOriginUrl.port = ''
|
||||
const signerOrigin = signerOriginUrl.origin
|
||||
const ready = new MessageEvent('message', {
|
||||
data: { type: 'archipelago:signer-ready' },
|
||||
origin: signerOrigin,
|
||||
})
|
||||
Object.defineProperty(ready, 'source', { value: frame.contentWindow })
|
||||
window.dispatchEvent(ready)
|
||||
|
||||
void providerWindow.nostr!.getPublicKey()
|
||||
expect(postMessage).not.toHaveBeenCalled()
|
||||
|
||||
window.dispatchEvent(new Event('load'))
|
||||
expect(postMessage.mock.calls.map(call => (call[0] as { type: string }).type))
|
||||
.toEqual(['archipelago:signer-init', 'nostr-request'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user