Skip to content
Open
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
@@ -1,22 +1,16 @@
import {prepareAdminStoreGraphQLContext} from './admin-context.js'
import {clearStoredStoreAppSession} from '../auth/session-store.js'
import {fetchPublicApiVersions} from './admin-transport.js'
import {loadStoredStoreSession} from '../auth/session-lifecycle.js'
import {STORE_AUTH_APP_CLIENT_ID} from '../auth/config.js'
import {AbortError} from '@shopify/cli-kit/node/error'
import {fetchApiVersions} from '@shopify/cli-kit/node/api/admin'
import {beforeEach, describe, expect, test, vi} from 'vitest'

vi.mock('../auth/session-store.js')
vi.mock('../auth/session-lifecycle.js', () => ({loadStoredStoreSession: vi.fn()}))
vi.mock('@shopify/cli-kit/node/api/admin', async () => {
const actual = await vi.importActual<typeof import('@shopify/cli-kit/node/api/admin')>(
'@shopify/cli-kit/node/api/admin',
)
return {
...actual,
fetchApiVersions: vi.fn(),
}
})
vi.mock('./admin-transport.js', () => ({
fetchPublicApiVersions: vi.fn(),
// runAdminStoreGraphQLOperation isn't exercised here, but we re-export it for type completeness.
runAdminStoreGraphQLOperation: vi.fn(),
}))

describe('prepareAdminStoreGraphQLContext', () => {
const store = 'shop.myshopify.com'
Expand All @@ -32,26 +26,23 @@ describe('prepareAdminStoreGraphQLContext', () => {

beforeEach(() => {
vi.mocked(loadStoredStoreSession).mockResolvedValue(storedSession)
vi.mocked(fetchApiVersions).mockResolvedValue([
vi.mocked(fetchPublicApiVersions).mockResolvedValue([
{handle: '2025-10', supported: true},
{handle: '2025-07', supported: true},
{handle: 'unstable', supported: false},
] as any)
])
})

test('returns the stored admin session, version, and full auth session', async () => {
const result = await prepareAdminStoreGraphQLContext({store})

expect(loadStoredStoreSession).toHaveBeenCalledWith(store)
expect(fetchApiVersions).toHaveBeenCalledWith({
token: 'token',
storeFqdn: store,
expect(fetchPublicApiVersions).toHaveBeenCalledWith({
adminSession: {token: 'token', storeFqdn: store},
session: storedSession,
})
expect(result).toEqual({
adminSession: {
token: 'token',
storeFqdn: store,
},
adminSession: {token: 'token', storeFqdn: store},
version: '2025-10',
session: storedSession,
})
Expand All @@ -68,9 +59,9 @@ describe('prepareAdminStoreGraphQLContext', () => {

const result = await prepareAdminStoreGraphQLContext({store})

expect(fetchApiVersions).toHaveBeenCalledWith({
token: 'fresh-token',
storeFqdn: store,
expect(fetchPublicApiVersions).toHaveBeenCalledWith({
adminSession: {token: 'fresh-token', storeFqdn: store},
session: refreshedSession,
})
expect(result.adminSession.token).toBe('fresh-token')
expect(result.session).toEqual(refreshedSession)
Expand All @@ -82,36 +73,27 @@ describe('prepareAdminStoreGraphQLContext', () => {
expect(result.version).toBe('2025-07')
})

test('allows unstable without validating against fetched versions', async () => {
test('allows unstable without consulting the transport, but still loads the stored session', async () => {
const result = await prepareAdminStoreGraphQLContext({store, userSpecifiedVersion: 'unstable'})

expect(result.version).toBe('unstable')
expect(fetchApiVersions).not.toHaveBeenCalled()
})

test('clears the current stored auth and prompts re-auth with real scopes when API version lookup gets invalid auth', async () => {
vi.mocked(fetchApiVersions).mockRejectedValue(
new AbortError(`Error connecting to your store ${store}: unauthorized 401 {}`),
)

await expect(prepareAdminStoreGraphQLContext({store})).rejects.toMatchObject({
message: `Stored app authentication for ${store} is no longer valid.`,
tryMessage: 'To re-authenticate, run:',
nextSteps: [[{command: `shopify store auth --store ${store} --scopes read_products,write_orders`}]],
expect(loadStoredStoreSession).toHaveBeenCalledWith(store)
expect(result).toEqual({
adminSession: {token: 'token', storeFqdn: store},
version: 'unstable',
session: storedSession,
})
expect(clearStoredStoreAppSession).toHaveBeenCalledWith(store, '42')
})

test('rethrows unrelated API version lookup failures', async () => {
vi.mocked(fetchApiVersions).mockRejectedValue(new AbortError('upstream exploded'))

await expect(prepareAdminStoreGraphQLContext({store})).rejects.toThrow('upstream exploded')
expect(clearStoredStoreAppSession).not.toHaveBeenCalled()
expect(fetchPublicApiVersions).not.toHaveBeenCalled()
})

test('throws when the requested API version is invalid', async () => {
await expect(prepareAdminStoreGraphQLContext({store, userSpecifiedVersion: '1999-01'})).rejects.toThrow(
'Invalid API version',
)
})

test('rethrows whatever the transport raises (errors are owned by the transport)', async () => {
vi.mocked(fetchPublicApiVersions).mockRejectedValue(new AbortError('upstream exploded'))

await expect(prepareAdminStoreGraphQLContext({store})).rejects.toThrow('upstream exploded')
})
})
24 changes: 2 additions & 22 deletions packages/store/src/cli/services/store/execute/admin-context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {throwReauthenticateStoreAuthError} from '../auth/recovery.js'
import {clearStoredStoreAppSession} from '../auth/session-store.js'
import {fetchPublicApiVersions} from './admin-transport.js'
import {loadStoredStoreSession} from '../auth/session-lifecycle.js'
import {fetchApiVersions} from '@shopify/cli-kit/node/api/admin'
import {AbortError} from '@shopify/cli-kit/node/error'
import type {AdminSession} from '@shopify/cli-kit/node/session'
import type {StoredStoreAppSession} from '../auth/session-store.js'
Expand All @@ -21,25 +19,7 @@ async function resolveApiVersion(options: {

if (userSpecifiedVersion === 'unstable') return userSpecifiedVersion

let availableVersions
try {
availableVersions = await fetchApiVersions(adminSession)
} catch (error) {
if (
error instanceof AbortError &&
error.message.includes(`Error connecting to your store ${adminSession.storeFqdn}:`) &&
/\b(?:401|404)\b/.test(error.message)
) {
clearStoredStoreAppSession(session.store, session.userId)
throwReauthenticateStoreAuthError(
`Stored app authentication for ${session.store} is no longer valid.`,
session.store,
session.scopes.join(','),
)
}

throw error
}
const availableVersions = await fetchPublicApiVersions({adminSession, session})

if (!userSpecifiedVersion) {
const supportedVersions = availableVersions.filter((version) => version.supported).map((version) => version.handle)
Expand Down
Loading
Loading