Skip to content

Commit bbd7ed1

Browse files
authored
Redirect user to add new Studio access token on 401 response (#3311)
1 parent 2f9288d commit bbd7ed1

File tree

3 files changed

+97
-5
lines changed

3 files changed

+97
-5
lines changed

extension/src/patch.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { commands } from 'vscode'
12
import omit from 'lodash.omit'
2-
import fetch, { Response } from 'node-fetch'
3+
import fetch, { Response as FetchResponse } from 'node-fetch'
34
import {
45
EXPERIMENT_WORKSPACE_ID,
56
ExperimentFields,
@@ -9,7 +10,10 @@ import {
910
} from './cli/dvc/contract'
1011
import { AvailableCommands, InternalCommands } from './commands/internal'
1112
import { Args, ExperimentFlag } from './cli/dvc/constants'
13+
import { Response as UserResponse } from './vscode/response'
1214
import { Toast } from './vscode/toast'
15+
import { Modal } from './vscode/modal'
16+
import { RegisteredCommands } from './commands/external'
1317

1418
export const STUDIO_ENDPOINT = 'https://studio.iterative.ai/api/live'
1519

@@ -93,7 +97,7 @@ const collectExperimentDetails = (
9397
const sendPostRequest = (
9498
studioAccessToken: string,
9599
body: StartRequestBody | DataRequestBody | DoneRequestBody
96-
): Promise<Response> =>
100+
): Promise<FetchResponse> =>
97101
fetch(STUDIO_ENDPOINT, {
98102
body: JSON.stringify(body),
99103
headers: {
@@ -103,6 +107,16 @@ const sendPostRequest = (
103107
method: 'POST'
104108
})
105109

110+
const showUnauthorized = async () => {
111+
const response = await Modal.errorWithOptions(
112+
'The current Studio access token is invalid. Please add a new token.',
113+
UserResponse.SHOW
114+
)
115+
if (response === UserResponse.SHOW) {
116+
return commands.executeCommand(RegisteredCommands.CONNECT_SHOW)
117+
}
118+
}
119+
106120
const shareWithProgress = (
107121
experimentDetails: ExperimentDetails,
108122
repoUrl: string,
@@ -121,7 +135,16 @@ const shareWithProgress = (
121135
increment: 0,
122136
message: 'Initializing experiment...'
123137
})
124-
await sendPostRequest(studioAccessToken, { ...base, type: 'start' })
138+
const response = await sendPostRequest(studioAccessToken, {
139+
...base,
140+
type: 'start'
141+
})
142+
143+
if (response.status === 401) {
144+
progress.report({ increment: 100, message: 'Access unauthorized' })
145+
void showUnauthorized()
146+
return Toast.delayProgressClosing()
147+
}
125148

126149
progress.report({ increment: 33, message: 'Sending data...' })
127150
await sendPostRequest(studioAccessToken, {

extension/src/test/suite/patch.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { join } from 'path'
22
import { afterEach, beforeEach, describe, it, suite } from 'mocha'
3-
import { restore, stub } from 'sinon'
3+
import { restore, spy, stub } from 'sinon'
44
import { expect } from 'chai'
55
import * as Fetch from 'node-fetch'
6+
import { commands } from 'vscode'
67
import { buildInternalCommands, closeAllEditors } from './util'
78
import { PROGRESS_TEST_TIMEOUT } from './timeouts'
89
import { Disposable } from '../../extension'
@@ -12,6 +13,9 @@ import expShowFixture from '../fixtures/expShow/base/output'
1213
import { dvcDemoPath } from '../util'
1314
import { ExperimentFields } from '../../cli/dvc/contract'
1415
import { Toast } from '../../vscode/toast'
16+
import { Modal } from '../../vscode/modal'
17+
import { Response } from '../../vscode/response'
18+
import { RegisteredCommands } from '../../commands/external'
1519

1620
suite('Patch Test Suite', () => {
1721
const disposable = Disposable.fn()
@@ -27,7 +31,7 @@ suite('Patch Test Suite', () => {
2731

2832
describe('exp push patch', () => {
2933
it('should share an experiment to Studio', async () => {
30-
const mockFetch = stub(Fetch, 'default').resolves(undefined)
34+
const mockFetch = stub(Fetch, 'default').resolves({} as Fetch.Response)
3135
const mockStudioAccessToken = 'isat_12123123123123123'
3236
const mockRepoUrl = 'https://github.com/iterative/vscode-dvc-demo'
3337

@@ -98,6 +102,66 @@ suite('Patch Test Suite', () => {
98102
})
99103
}).timeout(PROGRESS_TEST_TIMEOUT)
100104

105+
it('should show an error modal if Studio returns a 401 response', async () => {
106+
const mockFetch = stub(Fetch, 'default').resolves({
107+
status: 401
108+
} as Fetch.Response)
109+
const mockStudioAccessToken = 'isat_12123123123123123'
110+
const mockRepoUrl = 'https://github.com/iterative/vscode-dvc-demo'
111+
112+
const executeCommandSpy = spy(commands, 'executeCommand')
113+
114+
const { internalCommands, gitReader, dvcReader } =
115+
buildInternalCommands(disposable)
116+
117+
const mockGetRemoteUrl = stub(gitReader, 'getRemoteUrl').resolves(
118+
mockRepoUrl
119+
)
120+
const mockExpShow = stub(dvcReader, 'expShow').resolves(expShowFixture)
121+
122+
const mockErrorWithOptions = stub(Modal, 'errorWithOptions').resolves(
123+
Response.SHOW
124+
)
125+
126+
registerPatchCommand(internalCommands)
127+
128+
await internalCommands.executeCommand(
129+
AvailableCommands.EXP_PUSH,
130+
mockStudioAccessToken,
131+
dvcDemoPath,
132+
'exp-e7a67'
133+
)
134+
135+
expect(mockGetRemoteUrl).to.be.calledOnce
136+
expect(mockExpShow).to.be.calledOnce
137+
expect(mockFetch).to.be.calledOnce
138+
expect(mockErrorWithOptions).to.be.calledOnce
139+
expect(executeCommandSpy).to.be.calledWithExactly(
140+
RegisteredCommands.CONNECT_SHOW
141+
)
142+
143+
const { name } = expShowFixture[
144+
'53c3851f46955fa3e2b8f6e1c52999acc8c9ea77'
145+
]['4fb124aebddb2adf1545030907687fa9a4c80e70'].data as ExperimentFields
146+
147+
const baseBody = {
148+
baseline_sha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77',
149+
client: 'vscode',
150+
name,
151+
repo_url: mockRepoUrl
152+
}
153+
const headers = {
154+
Authorization: `token ${mockStudioAccessToken}`,
155+
'Content-type': 'application/json'
156+
}
157+
158+
expect(mockFetch).to.be.calledWithExactly(STUDIO_ENDPOINT, {
159+
body: JSON.stringify({ ...baseBody, type: 'start' }),
160+
headers,
161+
method: 'POST'
162+
})
163+
}).timeout(PROGRESS_TEST_TIMEOUT)
164+
101165
it('should show an error message if the experiment cannot be found', async () => {
102166
const mockShowError = stub(Toast, 'showError').resolves(undefined)
103167

extension/src/vscode/modal.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { window } from 'vscode'
22
import { Response } from './response'
33

44
enum Level {
5+
ERROR = 'Error',
56
INFORMATION = 'Information',
67
WARNING = 'Warning'
78
}
@@ -15,6 +16,10 @@ export class Modal {
1516
return Modal.show(Level.WARNING, text, ...items)
1617
}
1718

19+
public static errorWithOptions(text: string, ...items: Response[]) {
20+
return Modal.show(Level.ERROR, text, ...items)
21+
}
22+
1823
private static show(
1924
level: Level,
2025
message: string,

0 commit comments

Comments
 (0)