Skip to content

Commit fb7cf15

Browse files
committed
chore: add config resolution test
Signed-off-by: Frederik Bußmann <frederik@bussmann.io>
1 parent 5fd4777 commit fb7cf15

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

test/unit/config-resolve.test.ts

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import type { Nuxt } from '@nuxt/schema'
2+
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
const installed = new Set<string>()
6+
const persisted: Array<[string, string]> = []
7+
8+
vi.mock('../../src/utils/install', () => ({
9+
isInstalled: (id: string) => installed.has(id),
10+
isResolvableFrom: () => true,
11+
}))
12+
13+
vi.mock('../../src/utils/session', async importOriginal => ({
14+
...await importOriginal<typeof import('../../src/utils/session')>(),
15+
persistSessionPassword: (rootDir: string, password: string) => {
16+
persisted.push([rootDir, password])
17+
18+
return Promise.resolve()
19+
},
20+
}))
21+
22+
const { configSchema, publicConfigSchema } = await import('../../src/schemas')
23+
24+
const API_URL = 'https://shopify.com/1234/account/customer/api/2026-04/graphql'
25+
26+
function nuxtStub(options: Record<string, unknown> = {}) {
27+
return { options: { ssr: true, dev: false, rootDir: '/tmp/shop', ...options } } as unknown as Nuxt
28+
}
29+
30+
async function resolve(input: Record<string, unknown>, nuxt = nuxtStub()) {
31+
return parse({ ...input, _nuxt: nuxt })
32+
}
33+
34+
async function parse(input: Record<string, unknown>) {
35+
const config = await configSchema.parseAsync(input)
36+
37+
return { config, publicConfig: publicConfigSchema.parse(config) }
38+
}
39+
40+
const storefront = { publicAccessToken: 'tok' }
41+
42+
beforeEach(() => {
43+
installed.clear()
44+
installed.add('@shopify/hydrogen')
45+
installed.add('@shopify/hydrogen-react')
46+
47+
persisted.length = 0
48+
49+
delete process.env.NUXT_SHOPIFY_CLIENTS_CUSTOMER_ACCOUNT_SESSION_PASSWORD
50+
51+
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ json: () => Promise.resolve({ graphql_api: API_URL }) })))
52+
})
53+
54+
describe('peer dependency requirements', () => {
55+
it('keeps analytics enabled when its dependencies and token are present', async () => {
56+
const { config, publicConfig } = await resolve({ name: 'shop', clients: { storefront }, analytics: { storefrontId: '1' } })
57+
58+
expect(config.analytics).toMatchObject({ storefrontId: '1' })
59+
expect(publicConfig.analytics).toMatchObject({ storefrontId: '1' })
60+
})
61+
62+
it('disables analytics in both configs when hydrogen-react is missing', async () => {
63+
installed.delete('@shopify/hydrogen-react')
64+
65+
const { config, publicConfig } = await resolve({ name: 'shop', clients: { storefront }, analytics: true })
66+
67+
expect(config.analytics).toBe(false)
68+
expect(publicConfig.analytics).toBe(false)
69+
})
70+
71+
it('disables analytics in both configs when no public storefront token is reachable', async () => {
72+
const { config, publicConfig } = await resolve({
73+
name: 'shop',
74+
clients: { storefront: { privateAccessToken: 'private' } },
75+
analytics: true,
76+
})
77+
78+
expect(config.analytics).toBe(false)
79+
expect(publicConfig.analytics).toBe(false)
80+
})
81+
82+
it('accepts a consent token in place of the storefront token', async () => {
83+
const { config } = await resolve({
84+
name: 'shop',
85+
clients: { storefront: { privateAccessToken: 'private' } },
86+
analytics: { consent: { storefrontAccessToken: 'consent-token' } },
87+
})
88+
89+
expect(config.analytics).toMatchObject({ consent: { storefrontAccessToken: 'consent-token' } })
90+
})
91+
92+
it('disables the customer account client in both configs when hydrogen is missing', async () => {
93+
installed.delete('@shopify/hydrogen')
94+
95+
const { config, publicConfig } = await resolve({
96+
name: 'shop',
97+
clients: { storefront, customerAccount: { clientId: 'cid' } },
98+
})
99+
100+
expect(config.clients.customerAccount).toBeUndefined()
101+
expect(publicConfig.clients.customerAccount).toBeUndefined()
102+
})
103+
})
104+
105+
describe('proxy requires SSR', () => {
106+
const input = { name: 'shop', clients: { storefront, customerAccount: { clientId: 'cid' } } }
107+
108+
it('keeps the proxies when rendering on the server', async () => {
109+
const { config, publicConfig } = await resolve(input)
110+
111+
expect(config.clients.storefront?.proxy).toEqual({ path: '_proxy/storefront' })
112+
expect(publicConfig.clients.storefront?.proxy).toEqual({ path: '_proxy/storefront' })
113+
expect(publicConfig.clients.customerAccount?.proxy).toEqual({ path: '_proxy/customer-account' })
114+
})
115+
116+
it('disables every proxy in both configs when ssr is off', async () => {
117+
const { config, publicConfig } = await resolve(input, nuxtStub({ ssr: false }))
118+
119+
expect(config.clients.storefront?.proxy).toBe(false)
120+
expect(config.clients.customerAccount?.proxy).toBe(false)
121+
expect(publicConfig.clients.storefront?.proxy).toBe(false)
122+
expect(publicConfig.clients.customerAccount?.proxy).toBe(false)
123+
})
124+
125+
it('disables every proxy in both configs when prerendering the whole app', async () => {
126+
const { config, publicConfig } = await resolve(input, nuxtStub({ _generate: true }))
127+
128+
expect(config.clients.storefront?.proxy).toBe(false)
129+
expect(publicConfig.clients.storefront?.proxy).toBe(false)
130+
})
131+
132+
it('leaves an explicit opt-out alone', async () => {
133+
const { config } = await resolve({ name: 'shop', clients: { storefront: { ...storefront, proxy: false } } })
134+
135+
expect(config.clients.storefront?.proxy).toBe(false)
136+
})
137+
})
138+
139+
describe('customer account api url', () => {
140+
const input = { name: 'shop', clients: { customerAccount: { clientId: 'cid' } } }
141+
142+
it('resolves the url from the well-known endpoint into both configs', async () => {
143+
const { config, publicConfig } = await resolve(input)
144+
145+
expect(fetch).toHaveBeenCalledWith('https://shop.myshopify.com/.well-known/customer-account-api')
146+
expect(config.clients.customerAccount?.apiUrl).toBe(API_URL)
147+
expect(publicConfig.clients.customerAccount?.apiUrl).toBe(API_URL)
148+
})
149+
150+
it('prefers a configured url and skips the lookup', async () => {
151+
const configured = 'https://shopify.com/9/account/customer/api/2026-04/graphql'
152+
153+
const { config } = await resolve({ name: 'shop', clients: { customerAccount: { clientId: 'cid', apiUrl: configured } } })
154+
155+
expect(fetch).not.toHaveBeenCalled()
156+
expect(config.clients.customerAccount?.apiUrl).toBe(configured)
157+
})
158+
159+
it('leaves the url unset when the lookup fails', async () => {
160+
vi.stubGlobal('fetch', vi.fn(() => Promise.reject(new Error('offline'))))
161+
162+
const { config } = await resolve(input)
163+
164+
expect(config.clients.customerAccount?.apiUrl).toBeUndefined()
165+
})
166+
})
167+
168+
describe('customer account session password', () => {
169+
const input = { name: 'shop', clients: { customerAccount: { clientId: 'cid' } } }
170+
171+
it('uses the environment variable when set', async () => {
172+
process.env.NUXT_SHOPIFY_CLIENTS_CUSTOMER_ACCOUNT_SESSION_PASSWORD = 'x'.repeat(48)
173+
174+
const { config } = await resolve(input)
175+
176+
expect(config.clients.customerAccount?.session.password).toBe('x'.repeat(48))
177+
expect(persisted).toHaveLength(0)
178+
})
179+
180+
it('generates and persists one in development', async () => {
181+
const { config } = await resolve(input, nuxtStub({ dev: true }))
182+
183+
const password = config.clients.customerAccount?.session.password
184+
185+
expect(password).toBeTruthy()
186+
expect(persisted).toEqual([['/tmp/shop', password]])
187+
})
188+
189+
it('never generates one in production', async () => {
190+
const { config } = await resolve(input)
191+
192+
expect(config.clients.customerAccount?.session.password).toBeUndefined()
193+
expect(persisted).toHaveLength(0)
194+
})
195+
196+
it('never leaks the password into the public config', async () => {
197+
const { publicConfig } = await resolve(input, nuxtStub({ dev: true }))
198+
199+
expect(publicConfig.clients.customerAccount).not.toHaveProperty('session')
200+
expect(JSON.stringify(publicConfig)).not.toContain(persisted[0]![1])
201+
})
202+
})
203+
204+
describe('without a nuxt instance', () => {
205+
it('stays pure — no lookups, no writes, no environment gating', async () => {
206+
installed.clear()
207+
208+
const { config } = await parse({
209+
name: 'shop',
210+
clients: { storefront, customerAccount: { clientId: 'cid' } },
211+
analytics: true,
212+
})
213+
214+
expect(fetch).not.toHaveBeenCalled()
215+
expect(persisted).toHaveLength(0)
216+
expect(config.analytics).toMatchObject({ autoPageView: true })
217+
expect(config.clients.customerAccount).toBeDefined()
218+
expect(config.clients.storefront?.proxy).toEqual({ path: '_proxy/storefront' })
219+
})
220+
})

0 commit comments

Comments
 (0)