Skip to content

Commit 4103f5a

Browse files
authored
Merge branch 'develop' into issue-30198
2 parents ee991a4 + e3a7994 commit 4103f5a

File tree

6 files changed

+90
-32
lines changed

6 files changed

+90
-32
lines changed

packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import path from 'path'
22
import os from 'os'
3-
import { ensureDir, copy, readFile } from 'fs-extra'
3+
import { ensureDir, copy, readFile, remove } from 'fs-extra'
44
import { StudioManager } from '../../studio'
55
import tar from 'tar'
66
import { verifySignatureFromFile } from '../../encryption'
7-
import crypto from 'crypto'
87
import fs from 'fs'
98
import fetch from 'cross-fetch'
109
import { agent } from '@packages/network'
@@ -47,6 +46,10 @@ const downloadStudioBundleToTempDirectory = async ({ studioUrl, projectId }: Opt
4746
encrypt: 'signed',
4847
})
4948

49+
if (!response.ok) {
50+
throw new Error(`Failed to download studio bundle: ${response.statusText}`)
51+
}
52+
5053
responseSignature = response.headers.get('x-cypress-signature')
5154

5255
await new Promise<void>((resolve, reject) => {
@@ -77,26 +80,12 @@ const downloadStudioBundleToTempDirectory = async ({ studioUrl, projectId }: Opt
7780
}
7881
}
7982

80-
const getTarHash = (): Promise<string> => {
81-
let hash = ''
82-
83-
return new Promise<string>((resolve, reject) => {
84-
fs.createReadStream(bundlePath)
85-
.pipe(crypto.createHash('sha256'))
86-
.setEncoding('base64url')
87-
.on('data', (data) => {
88-
hash += String(data)
89-
})
90-
.on('error', reject)
91-
.on('close', () => {
92-
resolve(hash)
93-
})
94-
})
95-
}
96-
9783
export const retrieveAndExtractStudioBundle = async ({ studioUrl, projectId }: Options): Promise<{ studioHash: string | undefined }> => {
84+
// The studio hash is the last part of the studio URL, after the last slash and before the extension
85+
const studioHash = studioUrl.split('/').pop()?.split('.')[0]
86+
9887
// First remove studioPath to ensure we have a clean slate
99-
await fs.promises.rm(studioPath, { recursive: true, force: true })
88+
await remove(studioPath)
10089
await ensureDir(studioPath)
10190

10291
// Note: CYPRESS_LOCAL_STUDIO_PATH is stripped from the binary, effectively removing this code path
@@ -112,8 +101,6 @@ export const retrieveAndExtractStudioBundle = async ({ studioUrl, projectId }: O
112101

113102
await downloadStudioBundleToTempDirectory({ studioUrl, projectId })
114103

115-
const studioHash = await getTarHash()
116-
117104
await tar.extract({
118105
file: bundlePath,
119106
cwd: studioPath,
@@ -177,6 +164,6 @@ export const getAndInitializeStudioManager = async ({ studioUrl, projectId, clou
177164
studioMethod: 'getAndInitializeStudioManager',
178165
})
179166
} finally {
180-
await fs.promises.rm(bundlePath, { force: true })
167+
await remove(bundlePath)
181168
}
182169
}

packages/server/lib/cloud/api/studio/post_studio_session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const postStudioSession = async ({ projectId }: GetStudioSessionOptions)
2828
})
2929

3030
if (!response.ok) {
31-
throw new Error('Failed to create studio session')
31+
throw new Error(`Failed to create studio session: ${response.statusText}`)
3232
}
3333

3434
const data = await response.json()

packages/server/lib/cloud/api/studio/report_studio_error.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export function reportStudioError ({
4040
// When developing locally, do not send to Sentry, but instead log to console.
4141
if (
4242
process.env.CYPRESS_LOCAL_STUDIO_PATH ||
43-
process.env.NODE_ENV === 'development'
43+
process.env.NODE_ENV === 'development' ||
44+
process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF
4445
) {
4546
// eslint-disable-next-line no-console
4647
console.error(`Error in ${studioMethod}:`, error)

packages/server/test/unit/cloud/api/studio/get_and_initialize_studio_manager_spec.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ describe('getAndInitializeStudioManager', () => {
2828
ensureStub = sinon.stub()
2929
copyStub = sinon.stub()
3030
readFileStub = sinon.stub()
31-
crossFetchStub = sinon.stub()
31+
crossFetchStub = sinon.stub().resolves({
32+
ok: true,
33+
statusText: 'OK',
34+
})
35+
3236
createReadStreamStub = sinon.stub()
3337
createWriteStreamStub = sinon.stub()
3438
verifySignatureFromFileStub = sinon.stub()
@@ -38,9 +42,6 @@ describe('getAndInitializeStudioManager', () => {
3842

3943
getAndInitializeStudioManager = (proxyquire('../lib/cloud/api/studio/get_and_initialize_studio_manager', {
4044
fs: {
41-
promises: {
42-
rm: rmStub.resolves(),
43-
},
4445
createReadStream: createReadStreamStub,
4546
createWriteStream: createWriteStreamStub,
4647
},
@@ -49,6 +50,7 @@ describe('getAndInitializeStudioManager', () => {
4950
platform: () => 'linux',
5051
},
5152
'fs-extra': {
53+
remove: rmStub.resolves(),
5254
ensureDir: ensureStub.resolves(),
5355
copy: copyStub.resolves(),
5456
readFile: readFileStub.resolves('console.log("studio script")'),
@@ -110,6 +112,8 @@ describe('getAndInitializeStudioManager', () => {
110112
})
111113

112114
crossFetchStub.resolves({
115+
ok: true,
116+
statusText: 'OK',
113117
body: readStream,
114118
headers: {
115119
get: (header) => {
@@ -251,6 +255,8 @@ describe('getAndInitializeStudioManager', () => {
251255
})
252256

253257
crossFetchStub.resolves({
258+
ok: true,
259+
statusText: 'OK',
254260
body: readStream,
255261
headers: {
256262
get: (header) => {
@@ -298,7 +304,7 @@ describe('getAndInitializeStudioManager', () => {
298304
expect(studioManagerSetupStub).to.be.calledWithMatch({
299305
script: 'console.log("studio script")',
300306
studioPath: '/tmp/cypress/studio',
301-
studioHash: 'V8T1PKuSTK1h9gr-1Z2Wtx__bxTpCXWRZ57sKmPVTSs',
307+
studioHash: 'abc',
302308
})
303309
})
304310

@@ -318,6 +324,8 @@ describe('getAndInitializeStudioManager', () => {
318324

319325
crossFetchStub.onFirstCall().rejects(new HttpError('Failed to fetch', 'url', 502, 'Bad Gateway', 'Bad Gateway', sinon.stub()))
320326
crossFetchStub.onSecondCall().resolves({
327+
ok: true,
328+
statusText: 'OK',
321329
body: readStream,
322330
headers: {
323331
get: (header) => {
@@ -365,7 +373,7 @@ describe('getAndInitializeStudioManager', () => {
365373
expect(studioManagerSetupStub).to.be.calledWithMatch({
366374
script: 'console.log("studio script")',
367375
studioPath: '/tmp/cypress/studio',
368-
studioHash: 'V8T1PKuSTK1h9gr-1Z2Wtx__bxTpCXWRZ57sKmPVTSs',
376+
studioHash: 'abc',
369377
})
370378
})
371379

@@ -424,6 +432,43 @@ describe('getAndInitializeStudioManager', () => {
424432
})
425433
})
426434

435+
it('throws an error and returns a studio manager in error state if the response status is not ok', async () => {
436+
const mockGetCloudUrl = sinon.stub()
437+
const mockAdditionalHeaders = sinon.stub()
438+
const cloud = {
439+
getCloudUrl: mockGetCloudUrl,
440+
additionalHeaders: mockAdditionalHeaders,
441+
} as unknown as CloudDataSource
442+
443+
mockGetCloudUrl.returns('http://localhost:1234')
444+
mockAdditionalHeaders.resolves({
445+
a: 'b',
446+
c: 'd',
447+
})
448+
449+
crossFetchStub.resolves({
450+
ok: false,
451+
statusText: 'Some failure',
452+
})
453+
454+
const projectId = '12345'
455+
456+
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud })
457+
458+
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
459+
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
460+
expect(createInErrorManagerStub).to.be.calledWithMatch({
461+
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Failed to download studio bundle: Some failure')),
462+
cloudApi: {
463+
cloudUrl: 'http://localhost:1234',
464+
cloudHeaders: { a: 'b', c: 'd' },
465+
},
466+
studioHash: undefined,
467+
projectSlug: '12345',
468+
studioMethod: 'getAndInitializeStudioManager',
469+
})
470+
})
471+
427472
it('throws an error and returns a studio manager in error state if the signature verification fails', async () => {
428473
const mockGetCloudUrl = sinon.stub()
429474
const mockAdditionalHeaders = sinon.stub()
@@ -439,6 +484,8 @@ describe('getAndInitializeStudioManager', () => {
439484
})
440485

441486
crossFetchStub.resolves({
487+
ok: true,
488+
statusText: 'OK',
442489
body: readStream,
443490
headers: {
444491
get: (header) => {
@@ -501,6 +548,8 @@ describe('getAndInitializeStudioManager', () => {
501548
})
502549

503550
crossFetchStub.resolves({
551+
ok: true,
552+
statusText: 'OK',
504553
body: readStream,
505554
headers: {
506555
get: () => null,

packages/server/test/unit/cloud/api/studio/post_studio_session_spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ describe('postStudioSession', () => {
5454
it('should throw immediately if the response is not ok', async () => {
5555
crossFetchStub.resolves({
5656
ok: false,
57+
statusText: 'Some failure',
5758
json: () => {
5859
return Promise.resolve({
5960
error: 'Failed to create studio session',
@@ -63,7 +64,7 @@ describe('postStudioSession', () => {
6364

6465
await expect(postStudioSession({
6566
projectId: '12345',
66-
})).to.be.rejectedWith('Failed to create studio session')
67+
})).to.be.rejectedWith('Failed to create studio session: Some failure')
6768

6869
expect(crossFetchStub).to.have.been.calledOnce
6970
})

packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ describe('lib/cloud/api/studio/report_studio_error', () => {
6262
)
6363
})
6464

65+
it('logs error when CYPRESS_INTERNAL_E2E_TESTING_SELF is set', () => {
66+
sinon.stub(console, 'error')
67+
process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF = 'true'
68+
const error = new Error('test error')
69+
70+
reportStudioError({
71+
cloudApi,
72+
studioHash: 'abc123',
73+
projectSlug: 'test-project',
74+
error,
75+
studioMethod: 'testMethod',
76+
})
77+
78+
// eslint-disable-next-line no-console
79+
expect(console.error).to.have.been.calledWith(
80+
'Error in testMethod:',
81+
error,
82+
)
83+
})
84+
6585
it('converts non-Error objects to Error', () => {
6686
const error = 'string error'
6787

0 commit comments

Comments
 (0)