Skip to content

Commit

Permalink
Add tests for dev session process (#4873)
Browse files Browse the repository at this point in the history
<!--
  ☝️How to write a good PR title:
  - Prefix it with [Feature] (if applicable)
  - Start with a verb, for example: Add, Delete, Improve, Fix…
  - Give as much context as necessary and as little as possible
  - Use a draft PR while it’s a work in progress
-->

### WHY are these changes introduced?
Adds some tests for `dev-session` covering different cases when creating/updating the session

<!--
  Context about the problem that’s being addressed.
-->

### WHAT is this pull request doing?
Adds tests!
<!--
  Summary of the changes committed.
  Before / after screenshots appreciated for UI changes.
-->

### How to test your changes?
Nothing to test!
<!--
  Please, provide steps for the reviewer to test your changes locally.
-->

### Post-release steps

<!--
  If changes require post-release steps, for example merging and publishing some documentation changes,
  specify it in this section and add the label "includes-post-release-steps".
  If it doesn't, feel free to remove this section.
-->

### Measuring impact

How do we know this change was effective? Please choose one:

- [ ] n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
- [ ] Existing analytics will cater for this addition
- [ ] PR includes analytics changes to measure impact

### Checklist

- [ ] I've considered possible cross-platform impacts (Mac, Linux, Windows)
- [ ] I've considered possible [documentation](https://shopify.dev) changes
  • Loading branch information
isaacroldan authored Nov 22, 2024
2 parents 2a139a2 + 2d13c1b commit 95828d0
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 2 deletions.
184 changes: 184 additions & 0 deletions packages/app/src/cli/services/dev/processes/dev-session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {setupDevSessionProcess, pushUpdatesForDevSession} from './dev-session.js'
import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js'
import {AppLinkedInterface} from '../../../models/app/app.js'
import {AppEventWatcher} from '../app-events/app-event-watcher.js'
import {buildAppURLForWeb} from '../../../utilities/app/app-url.js'
import {
testAppLinked,
testDeveloperPlatformClient,
testUIExtension,
testWebhookExtensions,
} from '../../../models/app/app.test-data.js'
import {formData} from '@shopify/cli-kit/node/http'
import {describe, expect, test, vi, beforeEach} from 'vitest'
import {AbortSignal} from '@shopify/cli-kit/node/abort'
import {flushPromises} from '@shopify/cli-kit/node/promises'
import {writeFile} from '@shopify/cli-kit/node/fs'
import * as outputContext from '@shopify/cli-kit/node/ui/components'

vi.mock('@shopify/cli-kit/node/fs')
vi.mock('@shopify/cli-kit/node/archiver')
vi.mock('@shopify/cli-kit/node/http')
vi.mock('../../../utilities/app/app-url.js')
vi.mock('node-fetch')

describe('setupDevSessionProcess', () => {
test('returns a dev session process with correct configuration', async () => {
// Given
const options = {
app: {} as AppLinkedInterface,
apiKey: 'test-api-key',
developerPlatformClient: {} as DeveloperPlatformClient,
storeFqdn: 'test.myshopify.com',
url: 'https://test.dev',
organizationId: 'org123',
appId: 'app123',
appWatcher: {} as AppEventWatcher,
}

// When
const process = await setupDevSessionProcess(options)

// Then
expect(process).toEqual({
type: 'dev-session',
prefix: 'dev-session',
function: pushUpdatesForDevSession,
options: {
app: options.app,
apiKey: options.apiKey,
developerPlatformClient: options.developerPlatformClient,
storeFqdn: options.storeFqdn,
url: options.url,
organizationId: options.organizationId,
appId: options.appId,
appWatcher: options.appWatcher,
},
})
})
})

describe('pushUpdatesForDevSession', () => {
let stdout: any
let stderr: any
let options: any
let developerPlatformClient: any
let appWatcher: AppEventWatcher
let app: AppLinkedInterface

beforeEach(() => {
vi.mocked(formData).mockReturnValue({append: vi.fn(), getHeaders: vi.fn()} as any)
vi.mocked(writeFile).mockResolvedValue(undefined)
stdout = {write: vi.fn()}
stderr = {write: vi.fn()}
developerPlatformClient = testDeveloperPlatformClient()
app = testAppLinked()
appWatcher = new AppEventWatcher(app)

options = {
developerPlatformClient,
appWatcher,
storeFqdn: 'test.myshopify.com',
appId: 'app123',
organizationId: 'org123',
}
})

test('creates a new dev session successfully when receiving the app watcher start event', async () => {
// When
await pushUpdatesForDevSession({stderr, stdout, abortSignal: new AbortSignal()}, options)
await appWatcher.start()
await flushPromises()

// Then
expect(developerPlatformClient.devSessionCreate).toHaveBeenCalled()
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Ready'))
})

test('updates use the extension handle as the output prefix', async () => {
// When

const spyContext = vi.spyOn(outputContext, 'useConcurrentOutputContext')
await pushUpdatesForDevSession({stderr, stdout, abortSignal: new AbortSignal()}, options)
await appWatcher.start()
await flushPromises()

const extension = await testUIExtension()
appWatcher.emit('all', {app, extensionEvents: [{type: 'updated', extension}]})
await flushPromises()

// Then
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Updated'))
expect(spyContext).toHaveBeenCalledWith({outputPrefix: 'test-ui-extension', stripAnsi: false}, expect.anything())

// In theory this shouldn't be necessary, but vitest doesn't restore spies automatically.
// eslint-disable-next-line @shopify/cli/no-vi-manual-mock-clear
vi.restoreAllMocks()
})

test('handles user errors from dev session creation', async () => {
// Given
const userErrors = [{message: 'Test error', category: 'test'}]
developerPlatformClient.devSessionCreate = vi.fn().mockResolvedValue({devSessionCreate: {userErrors}})

// When
await appWatcher.start()
await pushUpdatesForDevSession({stderr, stdout, abortSignal: new AbortSignal()}, options)
await flushPromises()

// Then
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Error'))
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Test error'))
})

test('handles receiving an event before session is ready', async () => {
// When
await pushUpdatesForDevSession({stderr, stdout, abortSignal: new AbortSignal()}, options)
appWatcher.emit('all', {app, extensionEvents: [{type: 'updated', extension: testWebhookExtensions()}]})
await flushPromises()

// Then
expect(stdout.write).toHaveBeenCalledWith(
expect.stringContaining('Change detected, but dev session is not ready yet.'),
)
expect(developerPlatformClient.devSessionCreate).not.toHaveBeenCalled()
expect(developerPlatformClient.devSessionUpdate).not.toHaveBeenCalled()
})

test('handles user errors from dev session update', async () => {
// Given
const userErrors = [{message: 'Update error', category: 'test'}]
developerPlatformClient.devSessionUpdate = vi.fn().mockResolvedValue({devSessionUpdate: {userErrors}})

// When
await pushUpdatesForDevSession({stderr, stdout, abortSignal: new AbortSignal()}, options)
await appWatcher.start()
await flushPromises()
appWatcher.emit('all', {app, extensionEvents: [{type: 'updated', extension: testWebhookExtensions()}]})
await flushPromises()

// Then
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Error'))
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Update error'))
})

test('handles scope changes and displays action required message', async () => {
// Given
vi.mocked(buildAppURLForWeb).mockResolvedValue('https://test.myshopify.com/admin/apps/test')
const event = {extensionEvents: [{type: 'updated', extension: {handle: 'app-access'}}], app}
const contextSpy = vi.spyOn(outputContext, 'useConcurrentOutputContext')

// When
await pushUpdatesForDevSession({stderr, stdout, abortSignal: new AbortSignal()}, options)
await appWatcher.start()
await flushPromises()
appWatcher.emit('all', event)
await flushPromises()

// Then
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Updated'))
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Action required'))
expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Scopes updated'))
expect(contextSpy).toHaveBeenCalledWith({outputPrefix: 'dev-session', stripAnsi: false}, expect.anything())
})
})
7 changes: 5 additions & 2 deletions packages/app/src/cli/services/dev/processes/dev-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {performActionWithRetryAfterRecovery} from '@shopify/cli-kit/common/retry
import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components'
import {JsonMapType} from '@shopify/cli-kit/node/toml'
import {AbortError} from '@shopify/cli-kit/node/error'
import {isUnitTest} from '@shopify/cli-kit/node/context/local'
import {Writable} from 'stream'

interface DevSessionOptions {
Expand Down Expand Up @@ -83,6 +84,7 @@ export const pushUpdatesForDevSession: DevProcessFunction<DevSessionOptions> = a
) => {
const {developerPlatformClient, appWatcher} = options

isDevSessionReady = false
const refreshToken = async () => {
return developerPlatformClient.refreshToken()
}
Expand Down Expand Up @@ -155,8 +157,9 @@ async function handleDevSessionResult(
await processUserErrors(result.error, processOptions, processOptions.stdout)
}

// If we failed to create a session, exit the process
if (!isDevSessionReady) throw new AbortError('Failed to create dev session')
// If we failed to create a session, exit the process. Don't throw an error in tests as it can't be caught due to the
// async nature of the process.
if (!isDevSessionReady && !isUnitTest()) throw new AbortError('Failed to create dev session')
}

/**
Expand Down

0 comments on commit 95828d0

Please sign in to comment.