Files
zpan/server/services/licensing-cloud.test.ts
T
Jasper Van 0b65e2dc15 [v2.6] Integrate zpan with new cloud order flow and complete migration cleanup
fix(store): guard cloud order actions by org
2026-05-08 15:42:05 -04:00

178 lines
6.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
CloudInvalidResponseError,
CloudNetworkError,
CloudUnboundError,
createPairing,
pollPairing,
refreshEntitlement,
requestBoundCloudJson,
} from './licensing-cloud'
const BASE_URL = 'https://cloud.zpan.space'
function makeResponse(body: unknown, status = 200): Response {
return {
ok: status >= 200 && status < 300,
status,
statusText: status === 200 ? 'OK' : 'Error',
json: async () => body,
text: async () => JSON.stringify(body),
} as unknown as Response
}
describe('licensing-cloud', () => {
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn())
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('createPairing', () => {
it('sends POST to /api/pairings with instance info', async () => {
const payload = {
code: 'ABC-123',
pairingUrl: 'https://cloud.zpan.space/pair',
expiresAt: '2026-01-01T00:00:00Z',
}
vi.mocked(fetch).mockResolvedValueOnce(makeResponse(payload))
const result = await createPairing(BASE_URL, 'inst-1', 'My ZPan', 'zpan.example.com')
const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://cloud.zpan.space/api/pairings')
expect(init.method).toBe('POST')
const body = JSON.parse(init.body as string)
expect(body.instanceId).toBe('inst-1')
expect(body.instanceName).toBe('My ZPan')
expect(body.instanceHost).toBe('zpan.example.com')
expect(result).toEqual(payload)
})
it('throws on non-OK response', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ error: 'Bad Request' }, 400))
await expect(createPairing(BASE_URL, 'inst-1', 'ZPan', 'host')).rejects.toThrow('Cloud pairing failed')
})
it('throws CloudNetworkError on fetch failure', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'))
await expect(createPairing(BASE_URL, 'inst-1', 'ZPan', 'host')).rejects.toThrow(CloudNetworkError)
})
})
describe('pollPairing', () => {
it('sends GET to /api/pairings/:code', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ status: 'pending' }))
await pollPairing(BASE_URL, 'ABC-123')
const [url] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://cloud.zpan.space/api/pairings/ABC-123')
})
it('returns pending status', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ status: 'pending' }))
const result = await pollPairing(BASE_URL, 'CODE-1')
expect(result.status).toBe('pending')
})
it('returns approved status with refreshToken and entitlement', async () => {
const payload = {
status: 'approved',
refreshToken: 'rt-token',
certificate: 'v4.public.token',
}
vi.mocked(fetch).mockResolvedValueOnce(makeResponse(payload))
const result = await pollPairing(BASE_URL, 'CODE-2')
expect(result.status).toBe('approved')
expect(result.refreshToken).toBe('rt-token')
expect(result.certificate).toBe('v4.public.token')
})
it('throws on non-OK response', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ error: 'Not Found' }, 404))
await expect(pollPairing(BASE_URL, 'BAD')).rejects.toThrow('Cloud poll failed')
})
it('throws CloudNetworkError on fetch failure', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Timeout'))
await expect(pollPairing(BASE_URL, 'CODE')).rejects.toThrow(CloudNetworkError)
})
})
describe('refreshEntitlement', () => {
it('sends POST to /api/entitlements with Bearer token', async () => {
const payload = { refreshToken: 'new-rt', certificate: 'v4.public.newtoken' }
vi.mocked(fetch).mockResolvedValueOnce(makeResponse(payload))
const result = await refreshEntitlement(BASE_URL, 'old-rt')
const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://cloud.zpan.space/api/entitlements')
expect(init.method).toBe('POST')
expect(init.headers).toEqual({ Authorization: 'Bearer old-rt' })
expect(init.body).toBeUndefined()
expect(result.refreshToken).toBe('new-rt')
expect(result.certificate).toBe('v4.public.newtoken')
})
it('throws CloudUnboundError on 401', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ error: 'Unbound' }, 401))
await expect(refreshEntitlement(BASE_URL, 'old-rt')).rejects.toThrow(CloudUnboundError)
})
it('throws on other non-OK response', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ error: 'Server Error' }, 500))
await expect(refreshEntitlement(BASE_URL, 'old-rt')).rejects.toThrow('Cloud refresh failed')
})
it('throws on missing certificate', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ refreshToken: 'new-rt' }))
await expect(refreshEntitlement(BASE_URL, 'old-rt')).rejects.toThrow(CloudInvalidResponseError)
})
it('throws CloudNetworkError on fetch failure', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Connection refused'))
await expect(refreshEntitlement(BASE_URL, 'old-rt')).rejects.toThrow(CloudNetworkError)
})
it('throws CloudNetworkError for non-Error fetch failures', async () => {
vi.mocked(fetch).mockRejectedValueOnce('offline')
const result = refreshEntitlement(BASE_URL, 'old-rt')
await expect(result).rejects.toThrow(CloudNetworkError)
await expect(result).rejects.toThrow('Cloud network error')
})
})
describe('requestBoundCloudJson', () => {
it('sends PATCH requests with bound authorization and JSON payloads', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ state: 'revoked' }))
const result = await requestBoundCloudJson(BASE_URL, '/api/store/gift-cards/ZS123', 'rt-bound', {
method: 'PATCH',
payload: { disabled: true },
})
const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://cloud.zpan.space/api/store/gift-cards/ZS123')
expect(init.method).toBe('PATCH')
expect(init.headers).toEqual({ Authorization: 'Bearer rt-bound', 'Content-Type': 'application/json' })
expect(JSON.parse(init.body as string)).toEqual({ disabled: true })
expect(result).toEqual({ state: 'revoked' })
})
})
})