Skip to content

Commit 6072dba

Browse files
committed
refactor: simplify shared workspaces provider and tests
- collapse per-query branches into a readonly WORKSPACE_QUERY_CONFIG table - drop the unreachable re-entry branch in the workspace refresh loop - remove test-only getCurrentUserId; tests read getSnapshot via a helper - extract the repeated tree-view wiring in extension.ts into a helper - drive metadata tests through the real watcher over MockEventStream - move shared test doubles (session, workspaces client, flush) into testHelpers - replace describe-scoped state + beforeEach/afterEach with a setup() helper
1 parent adfc6d0 commit 6072dba

7 files changed

Lines changed: 525 additions & 585 deletions

File tree

src/deployment/deploymentManager.ts

Lines changed: 61 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as vscode from "vscode";
2-
31
import { CoderApi } from "../api/coderApi";
42
import {
53
CONFIG_CHANGE_DEBOUNCE_MS,
@@ -14,32 +12,21 @@ import { type OAuthSessionManager } from "../oauth/sessionManager";
1412
import { getAuthConfigWatchSettings } from "../settings/authConfig";
1513
import { type TelemetryService } from "../telemetry/service";
1614

15+
import { SessionStore, type SessionData } from "./sessionStore";
1716
import {
1817
DeploymentSchema,
1918
type Deployment,
2019
type DeploymentWithAuth,
2120
} from "./types";
2221

2322
import type { User } from "coder/site/src/api/typesGenerated";
23+
import type * as vscode from "vscode";
2424

2525
import type {
2626
WorkspaceSessionSnapshot,
2727
WorkspaceSessionState,
2828
} from "../workspace/session";
2929

30-
type DeploymentSessionSnapshot =
31-
| {
32-
readonly kind: "signedOut";
33-
readonly revision: number;
34-
readonly deployment: Deployment | null;
35-
}
36-
| {
37-
readonly kind: "signedIn";
38-
readonly revision: number;
39-
readonly deployment: Deployment;
40-
readonly user: User;
41-
};
42-
4330
/**
4431
* Manages deployment state for the extension.
4532
*
@@ -60,14 +47,8 @@ export class DeploymentManager
6047
private readonly logger: Logger;
6148
private readonly telemetryService: TelemetryService;
6249

63-
#session: DeploymentSessionSnapshot = {
64-
kind: "signedOut",
65-
revision: 0,
66-
deployment: null,
67-
};
68-
readonly #onDidChangeWorkspaceSession =
69-
new vscode.EventEmitter<WorkspaceSessionSnapshot>();
70-
public readonly onDidChange = this.#onDidChangeWorkspaceSession.event;
50+
readonly #sessionStore = new SessionStore();
51+
public readonly onDidChange = this.#sessionStore.onDidChange;
7152
#disposed = false;
7253
#authListenerDisposable: vscode.Disposable | undefined;
7354
#authConfigDisposable: vscode.Disposable | undefined;
@@ -106,51 +87,37 @@ export class DeploymentManager
10687
* Get the current deployment state.
10788
*/
10889
public getCurrentDeployment(): Deployment | null {
109-
return this.#session.deployment;
110-
}
111-
112-
public getCurrentUserId(): string | undefined {
113-
return this.#session.kind === "signedIn"
114-
? this.#session.user.id
115-
: undefined;
90+
return this.#sessionStore.current.deployment;
11691
}
11792

11893
public getSnapshot(): WorkspaceSessionSnapshot {
119-
if (this.#session.kind === "signedIn") {
120-
return {
121-
kind: "signedIn",
122-
revision: this.#session.revision,
123-
userId: this.#session.user.id,
124-
};
125-
}
126-
return { kind: "signedOut", revision: this.#session.revision };
94+
return this.#sessionStore.getSnapshot();
12795
}
12896

12997
/**
13098
* Check if we have an authenticated deployment (does not guarantee that the current auth data is valid).
13199
*/
132100
public isAuthenticated(): boolean {
133-
return this.#session.kind === "signedIn";
101+
return this.#sessionStore.current.kind === "signedIn";
134102
}
135103

136104
/**
137-
* Verify credentials and apply the deployment on success. Used for
138-
* fresh logins and for un-suspending a session after auth settings or
139-
* a token become valid again. Bails if state moved during the verify
140-
* (logout, another login, dispose), so callers don't need a race guard.
105+
* Verify credentials and apply the deployment on success, signing in. Used
106+
* for fresh logins and for un-suspending a session once auth settings or a
107+
* token become valid again. Bails if state moved during the verify (logout,
108+
* another login, dispose), so callers don't need a race guard.
141109
*/
142-
public async verifyAndApplyDeployment(
110+
public async verifyAndApplySession(
143111
deployment: Deployment & { token?: string },
144112
): Promise<boolean> {
145-
const sessionBefore = this.#session;
113+
const sessionBefore = this.#sessionStore.current;
146114
const token =
147115
deployment.token ??
148116
(await this.secretsManager.getSessionAuth(deployment.safeHostname))
149117
?.token;
150-
const tempClient = CoderApi.create(deployment.url, token, this.logger);
151118

152119
try {
153-
const user = await tempClient.getAuthenticatedUser();
120+
const user = await this.#verifyCredentials(deployment.url, token);
154121
if (this.#hasStateChangedSince(sessionBefore)) {
155122
return false;
156123
}
@@ -159,17 +126,31 @@ export class DeploymentManager
159126
} catch (e) {
160127
this.logger.warn("Failed to authenticate with deployment:", e);
161128
return false;
129+
}
130+
}
131+
132+
/**
133+
* Verify credentials with a throwaway client and return the authenticated
134+
* user. Throws if the credentials are rejected.
135+
*/
136+
async #verifyCredentials(
137+
url: string,
138+
token: string | undefined,
139+
): Promise<User> {
140+
const tempClient = CoderApi.create(url, token, this.logger);
141+
try {
142+
return await tempClient.getAuthenticatedUser();
162143
} finally {
163144
tempClient.dispose();
164145
}
165146
}
166147

167148
/** True if disposal, login, or a deployment switch raced our await. */
168-
#hasStateChangedSince(sessionBefore: DeploymentSessionSnapshot): boolean {
149+
#hasStateChangedSince(sessionBefore: SessionData): boolean {
169150
return (
170151
this.#disposed ||
171152
this.isAuthenticated() ||
172-
this.#session !== sessionBefore
153+
this.#sessionStore.current !== sessionBefore
173154
);
174155
}
175156

@@ -192,14 +173,17 @@ export class DeploymentManager
192173
this.client.setCredentials(deployment.url, deployment.token);
193174
}
194175

195-
const ourRef = this.setSignedIn(deploymentWithoutAuth, deployment.user);
176+
const ourRef = this.#sessionStore.signIn(
177+
deploymentWithoutAuth,
178+
deployment.user,
179+
);
196180
// Register before OAuth setup so background token refresh can update client credentials.
197181
this.registerAuthListener();
198182
this.updateAuthContexts(deployment.user);
199183

200184
await this.oauthSessionManager.setDeployment(deploymentWithoutAuth);
201185
// Bail if a concurrent write took over during the await.
202-
if (this.#session !== ourRef) {
186+
if (this.#sessionStore.current !== ourRef) {
203187
return;
204188
}
205189
await this.persistDeployment(deploymentWithoutAuth);
@@ -211,12 +195,12 @@ export class DeploymentManager
211195
public async clearDeployment(): Promise<void> {
212196
this.logger.debug(
213197
"Clearing deployment",
214-
this.#session.deployment?.safeHostname,
198+
this.#sessionStore.current.deployment?.safeHostname,
215199
);
216200
this.#authListenerDisposable?.dispose();
217201
this.#authListenerDisposable = undefined;
218-
this.setSignedOut(null);
219-
this.clearSessionSideEffects();
202+
this.#sessionStore.signOut(null);
203+
this.clearSideEffects();
220204
this.telemetryService.setDeploymentUrl("");
221205

222206
await this.secretsManager.setCurrentDeployment(undefined);
@@ -227,11 +211,11 @@ export class DeploymentManager
227211
* Auth listener remains active so recovery can happen automatically if tokens update.
228212
*/
229213
public suspendSession(): void {
230-
this.setSignedOut(this.#session.deployment);
231-
this.clearSessionSideEffects();
214+
this.#sessionStore.signOut(this.#sessionStore.current.deployment);
215+
this.clearSideEffects();
232216
}
233217

234-
private clearSessionSideEffects(): void {
218+
private clearSideEffects(): void {
235219
this.oauthSessionManager.clearDeployment();
236220
this.client.setCredentials(undefined, undefined);
237221
this.updateAuthContexts(undefined);
@@ -242,7 +226,7 @@ export class DeploymentManager
242226
this.#authListenerDisposable?.dispose();
243227
this.#authConfigDisposable?.dispose();
244228
this.#crossWindowSyncDisposable?.dispose();
245-
this.#onDidChangeWorkspaceSession.dispose();
229+
this.#sessionStore.dispose();
246230
}
247231

248232
/**
@@ -251,25 +235,28 @@ export class DeploymentManager
251235
* Also handles recovery from suspended session state.
252236
*/
253237
private registerAuthListener(): void {
254-
if (!this.#session.deployment) {
238+
const deployment = this.#sessionStore.current.deployment;
239+
if (!deployment) {
255240
return;
256241
}
257242

258243
// Capture hostname at registration time for the guard clause
259-
const safeHostname = this.#session.deployment.safeHostname;
244+
const safeHostname = deployment.safeHostname;
260245

261246
this.#authListenerDisposable?.dispose();
262247
this.logger.debug("Registering auth listener for hostname", safeHostname);
263248
this.#authListenerDisposable = this.secretsManager.onDidChangeSessionAuth(
264249
safeHostname,
265250
async (auth) => {
266-
if (this.#session.deployment?.safeHostname !== safeHostname) {
251+
if (
252+
this.#sessionStore.current.deployment?.safeHostname !== safeHostname
253+
) {
267254
return;
268255
}
269256

270257
if (auth) {
271258
if (this.isAuthenticated()) {
272-
await this.verifyAndUpdateAuthenticatedSession({
259+
await this.verifyAndUpdateSession({
273260
url: auth.url,
274261
safeHostname,
275262
token: auth.token,
@@ -278,7 +265,7 @@ export class DeploymentManager
278265
this.logger.debug(
279266
"Token updated after session suspended, recovering",
280267
);
281-
await this.verifyAndApplyDeployment({
268+
await this.verifyAndApplySession({
282269
url: auth.url,
283270
safeHostname,
284271
token: auth.token,
@@ -291,55 +278,24 @@ export class DeploymentManager
291278
);
292279
}
293280

294-
private async verifyAndUpdateAuthenticatedSession(
281+
private async verifyAndUpdateSession(
295282
deployment: Deployment & { token: string },
296283
): Promise<void> {
297-
const sessionBefore = this.#session;
298-
const tempClient = CoderApi.create(
299-
deployment.url,
300-
deployment.token,
301-
this.logger,
302-
);
303-
284+
const sessionBefore = this.#sessionStore.current;
304285
try {
305-
const user = await tempClient.getAuthenticatedUser();
306-
if (this.#disposed || this.#session !== sessionBefore) {
286+
const user = await this.#verifyCredentials(
287+
deployment.url,
288+
deployment.token,
289+
);
290+
if (this.#disposed || this.#sessionStore.current !== sessionBefore) {
307291
return;
308292
}
309293
await this.setDeployment({ ...deployment, user });
310294
} catch (e) {
311295
this.logger.warn("Failed to authenticate updated session:", e);
312-
} finally {
313-
tempClient.dispose();
314296
}
315297
}
316298

317-
private setSignedIn(
318-
deployment: Deployment,
319-
user: User,
320-
): DeploymentSessionSnapshot {
321-
this.#session = {
322-
kind: "signedIn",
323-
revision: this.#session.revision + 1,
324-
deployment,
325-
user,
326-
};
327-
this.#onDidChangeWorkspaceSession.fire(this.getSnapshot());
328-
return this.#session;
329-
}
330-
331-
private setSignedOut(
332-
deployment: Deployment | null,
333-
): DeploymentSessionSnapshot {
334-
this.#session = {
335-
kind: "signedOut",
336-
revision: this.#session.revision + 1,
337-
deployment,
338-
};
339-
this.#onDidChangeWorkspaceSession.fire(this.getSnapshot());
340-
return this.#session;
341-
}
342-
343299
private subscribeToAuthConfigChanges(): void {
344300
this.#authConfigDisposable = watchConfigurationChanges(
345301
getAuthConfigWatchSettings(),
@@ -363,14 +319,14 @@ export class DeploymentManager
363319
try {
364320
do {
365321
this.#recoveryPending = false;
366-
const snapshot = this.#session.deployment;
367-
if (this.#disposed || !snapshot || this.isAuthenticated()) {
322+
const deployment = this.#sessionStore.current.deployment;
323+
if (this.#disposed || !deployment || this.isAuthenticated()) {
368324
return;
369325
}
370326
this.logger.debug(
371327
"Authentication settings changed after session suspended, recovering",
372328
);
373-
await this.verifyAndApplyDeployment(snapshot);
329+
await this.verifyAndApplySession(deployment);
374330
} while (this.#recoveryPending);
375331
} catch (err) {
376332
this.logger.warn(
@@ -392,7 +348,7 @@ export class DeploymentManager
392348

393349
if (deployment) {
394350
this.logger.info("Deployment changed from another window");
395-
await this.verifyAndApplyDeployment(deployment);
351+
await this.verifyAndApplySession(deployment);
396352
}
397353
},
398354
);

0 commit comments

Comments
 (0)