Skip to content

Commit 8a20276

Browse files
committed
adding unit tests for fir app log streaming errors
1 parent 24254f9 commit 8a20276

File tree

1 file changed

+151
-1
lines changed

1 file changed

+151
-1
lines changed

packages/cli/test/unit/lib/run/log-displayer.unit.test.ts

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import {APIClient} from '@heroku-cli/command'
33
import {Config} from '@oclif/core'
44
import {CLIError} from '@oclif/core/lib/errors'
55
import {expect} from 'chai'
6+
import {EventEmitter} from 'node:events'
67
import * as nock from 'nock'
8+
import * as proxyquire from 'proxyquire'
9+
import * as sinon from 'sinon'
10+
import {stdout} from 'stdout-stderr'
11+
import heredoc from 'tsheredoc'
712
import logDisplayer from '../../../../src/lib/run/log-displayer'
8-
import {cedarApp} from '../../../fixtures/apps/fixtures'
13+
import {cedarApp, firApp} from '../../../fixtures/apps/fixtures'
914

1015
describe('logDisplayer', function () {
1116
let api: nock.Scope
@@ -140,4 +145,149 @@ describe('logDisplayer', function () {
140145
})
141146
})
142147
})
148+
149+
context('with a Fir app', function () {
150+
let api: nock.Scope
151+
let mockEventSourceInstance: EventEmitter
152+
let logDisplayerWithMock: typeof logDisplayer
153+
154+
// Mock EventSource class that extends EventEmitter
155+
class MockEventSource extends EventEmitter {
156+
url: string
157+
close: () => void
158+
static lastInstance: EventEmitter | null = null
159+
160+
constructor(url: string, _options?: {proxy?: string; headers?: Record<string, string>}) {
161+
super()
162+
this.url = url
163+
this.close = () => {
164+
this.removeAllListeners()
165+
}
166+
167+
// Store instance for test control
168+
MockEventSource.lastInstance = this
169+
mockEventSourceInstance = MockEventSource.lastInstance
170+
}
171+
172+
// EventSource uses addEventListener, not just on()
173+
addEventListener(event: string, handler: (e: any) => void) {
174+
this.on(event, handler)
175+
}
176+
}
177+
178+
beforeEach(function () {
179+
nock.cleanAll()
180+
api = nock('https://api.heroku.com', {
181+
reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'},
182+
}).get('/apps/my-fir-app')
183+
.reply(200, firApp)
184+
185+
sinon.stub(heroku, 'post').resolves({
186+
body: {logplex_url: 'https://logs.heroku.com/stream?tail=true&token=s3kr3t'},
187+
} as any)
188+
189+
logDisplayerWithMock = proxyquire('../../../../src/lib/run/log-displayer', {
190+
'@heroku/eventsource': MockEventSource,
191+
}).default
192+
})
193+
194+
afterEach(function () {
195+
api.done()
196+
sinon.restore()
197+
mockEventSourceInstance = null as any
198+
})
199+
200+
context('when the log server returns a 403 error before connection', function () {
201+
it('shows the IP address access error and exits', async function () {
202+
const promise = logDisplayerWithMock(heroku, {
203+
app: 'my-fir-app',
204+
tail: true,
205+
})
206+
207+
const waitForInstance = () => {
208+
if (mockEventSourceInstance) {
209+
mockEventSourceInstance.emit('error', {status: 403, message: null})
210+
} else {
211+
setImmediate(waitForInstance)
212+
}
213+
}
214+
215+
setImmediate(waitForInstance)
216+
217+
try {
218+
await promise
219+
} catch (error: unknown) {
220+
const {message, oclif} = error as CLIError
221+
expect(message).to.equal("You can't access this space from your IP address. Contact your team admin.")
222+
expect(oclif.exit).to.eq(1)
223+
}
224+
})
225+
})
226+
227+
context('when the log server returns a 403 error after connection', function () {
228+
it('shows the stream access expired error and exits', async function () {
229+
stdout.start()
230+
231+
const promise = logDisplayerWithMock(heroku, {
232+
app: 'my-fir-app',
233+
tail: true,
234+
})
235+
236+
const waitAndEmit = () => {
237+
if (mockEventSourceInstance) {
238+
mockEventSourceInstance.emit('message', {data: '2024-10-17T22:23:22.209776+00:00 app[web.1]: log line 1'})
239+
mockEventSourceInstance.emit('message', {data: '2024-10-17T22:23:23.032789+00:00 app[web.1]: log line 2'})
240+
setImmediate(() => {
241+
mockEventSourceInstance.emit('error', {status: 403, message: null})
242+
})
243+
} else {
244+
setImmediate(waitAndEmit)
245+
}
246+
}
247+
248+
setImmediate(waitAndEmit)
249+
250+
try {
251+
await promise
252+
} catch (error: unknown) {
253+
stdout.stop()
254+
const {message, oclif} = error as CLIError
255+
expect(message).to.equal('Log stream access expired. Please try again.')
256+
expect(oclif.exit).to.eq(1)
257+
}
258+
259+
expect(stdout.output).to.eq(heredoc`
260+
2024-10-17T22:23:22.209776+00:00 app[web.1]: log line 1
261+
2024-10-17T22:23:23.032789+00:00 app[web.1]: log line 2
262+
`)
263+
})
264+
})
265+
266+
context('when the log server returns a 404 error', function () {
267+
it('shows the stream access expired error and exits', async function () {
268+
const promise = logDisplayerWithMock(heroku, {
269+
app: 'my-fir-app',
270+
tail: true,
271+
})
272+
273+
const waitForInstance = () => {
274+
if (mockEventSourceInstance) {
275+
mockEventSourceInstance.emit('error', {status: 404, message: null})
276+
} else {
277+
setImmediate(waitForInstance)
278+
}
279+
}
280+
281+
setImmediate(waitForInstance)
282+
283+
try {
284+
await promise
285+
} catch (error: unknown) {
286+
const {message, oclif} = error as CLIError
287+
expect(message).to.equal('Log stream access expired. Please try again.')
288+
expect(oclif.exit).to.eq(1)
289+
}
290+
})
291+
})
292+
})
143293
})

0 commit comments

Comments
 (0)