Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('preview store client', () => {
shop: {id: 123, name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'},
placeholder_account_uuid: 'placeholder-uuid',
admin_api_token: 'shpat_token',
admin_api_scopes: ['read_themes', 'write_themes'],
access_url: 'https://app.shopify.com/auth/preview-store?token=access-token',
}),
)
Expand All @@ -106,15 +107,46 @@ describe('preview store client', () => {
shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'},
placeholderAccountUuid: 'placeholder-uuid',
adminApiToken: 'shpat_token',
adminApiScopes: ['read_themes', 'write_themes'],
accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token',
})
})

test('rejects the response when the backend omits admin API scopes', async () => {
vi.mocked(shopifyFetch).mockResolvedValueOnce(
response(201, {
shop: {id: 123, name: 'My Store', domain: 'x.myshopify.com'},
admin_api_token: 'shpat_token',
access_url: 'https://app.shopify.com/auth/preview-store?token=access-token',
}),
)

await expect(createPreviewStore({}, {storage: inMemoryStorage('instance-1')})).rejects.toThrow(
'Preview store creation response is missing required fields.',
)
})

test('drops non-string admin API scopes', async () => {
vi.mocked(shopifyFetch).mockResolvedValueOnce(
response(201, {
shop: {id: 123, name: 'My Store', domain: 'x.myshopify.com'},
admin_api_token: 'shpat_token',
admin_api_scopes: ['read_themes', 42, null, 'write_themes'],
access_url: 'https://app.shopify.com/auth/preview-store?token=access-token',
}),
)

const got = await createPreviewStore({}, {storage: inMemoryStorage('instance-1')})

expect(got.adminApiScopes).toEqual(['read_themes', 'write_themes'])
})

test('omits name and country variables when absent', async () => {
vi.mocked(shopifyFetch).mockResolvedValueOnce(
response(201, {
shop: {id: 123, name: 'My Store', domain: 'x.myshopify.com'},
admin_api_token: 'shpat_token',
admin_api_scopes: ['read_themes', 'write_themes'],
access_url: 'https://app.shopify.com/auth/preview-store?token=access-token',
}),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface PreviewStoreCreateResponse {
shop: PreviewStoreResponseShop
placeholderAccountUuid?: string
adminApiToken: string
adminApiScopes: string[]
accessUrl: string
}

Expand All @@ -53,6 +54,7 @@ interface RawPreviewStoreCreateResponse {
shop?: RawPreviewStoreResponseShop
placeholder_account_uuid?: unknown
admin_api_token?: unknown
admin_api_scopes?: unknown
access_url?: unknown
}

Expand Down Expand Up @@ -320,8 +322,13 @@ function narrowCreateResponse(parsed: RawPreviewStoreCreateResponse): PreviewSto
const accessUrl = typeof parsed.access_url === 'string' ? parsed.access_url : undefined
const placeholderAccountUuid =
typeof parsed.placeholder_account_uuid === 'string' ? parsed.placeholder_account_uuid : undefined
// The backend always returns `admin_api_scopes` for the granted Admin API token, so it is a
// required field. We still filter out any non-string entries defensively.
const adminApiScopes = Array.isArray(parsed.admin_api_scopes)
? parsed.admin_api_scopes.filter((scope): scope is string => typeof scope === 'string')
: undefined

if (!id || !name || !domain || !adminApiToken || !accessUrl) {
if (!id || !name || !domain || !adminApiToken || !accessUrl || !adminApiScopes) {
throw new AbortError(
'Preview store creation response is missing required fields.',
`Got: ${JSON.stringify(redactPreviewStoreResponse(parsed)).slice(0, 500)}`,
Expand All @@ -331,6 +338,7 @@ function narrowCreateResponse(parsed: RawPreviewStoreCreateResponse): PreviewSto
return {
shop: {id, name, domain},
adminApiToken,
adminApiScopes,
accessUrl,
...(placeholderAccountUuid ? {placeholderAccountUuid} : {}),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('preview store create service', () => {
shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'},
placeholderAccountUuid: 'placeholder-uuid',
adminApiToken: 'shpat_token',
adminApiScopes: ['read_themes', 'write_themes'],
accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token',
})),
setStoredStoreAppSession,
Expand All @@ -29,7 +30,7 @@ describe('preview store create service', () => {
clientId: STORE_AUTH_APP_CLIENT_ID,
userId: `${PREVIEW_USER_ID_PREFIX}placeholder-uuid`,
accessToken: 'shpat_token',
scopes: [],
scopes: ['read_themes', 'write_themes'],
acquiredAt: '2026-06-08T12:00:00.000Z',
kind: 'preview',
preview: {
Expand Down Expand Up @@ -68,6 +69,7 @@ describe('preview store create service', () => {
createPreviewStore: vi.fn(async () => ({
shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'},
adminApiToken: 'shpat_token',
adminApiScopes: [],
accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token',
})),
setStoredStoreAppSession,
Expand All @@ -78,7 +80,7 @@ describe('preview store create service', () => {
)

expect(setStoredStoreAppSession).toHaveBeenCalledWith(
expect.objectContaining({userId: `${PREVIEW_USER_ID_PREFIX}123`}),
expect.objectContaining({userId: `${PREVIEW_USER_ID_PREFIX}123`, scopes: []}),
)
expect(setLastSeenUserId).toHaveBeenCalledWith(`${PREVIEW_USER_ID_PREFIX}123`)
})
Expand All @@ -88,6 +90,7 @@ describe('preview store create service', () => {
const createPreviewStore = vi.fn(async () => ({
shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'},
adminApiToken: 'shpat_token',
adminApiScopes: [],
accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token',
}))

Expand Down Expand Up @@ -118,6 +121,7 @@ describe('preview store create service', () => {
createPreviewStore: vi.fn(async () => ({
shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'},
adminApiToken: 'shpat_token',
adminApiScopes: [],
accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token',
})),
setStoredStoreAppSession,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ async function persistPreviewStoreSession(
clientId: STORE_AUTH_APP_CLIENT_ID,
userId,
accessToken: response.adminApiToken,
scopes: [],
scopes: response.adminApiScopes,
acquiredAt,
kind: 'preview',
preview: {
Expand Down
Loading