mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-01 15:49:00 +08:00
0b65e2dc15
fix(store): guard cloud order actions by org
178 lines
6.5 KiB
TypeScript
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' })
|
|
})
|
|
})
|
|
})
|