Skip to content

Commit e285ad7

Browse files
Clean up dialogs for easier testing
1 parent e82fba2 commit e285ad7

11 files changed

Lines changed: 401 additions & 93 deletions

File tree

src/cloud/commands/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import * as vscode from "vscode"
21
import { trackCloudSignOut } from "../../utils/telemetry"
32
import type { AuthProvider } from "../types"
3+
import { ui } from "../ui/dialogs"
44

55
export async function signOut(authProvider: AuthProvider): Promise<boolean> {
6-
const confirm = await vscode.window.showWarningMessage(
6+
const confirm = await ui.showWarningMessage(
77
"Sign out of FastAPI Cloud?",
88
{ modal: true },
99
"Sign Out",

src/cloud/commands/deploy.ts

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,47 @@ import type { ApiService } from "../api"
99
import { AUTH_PROVIDER_ID } from "../auth"
1010
import type { ConfigService } from "../config"
1111
import {
12+
type App,
1213
type Config,
1314
type Deployment,
1415
DeploymentStatus,
1516
failedStatuses,
1617
statusMessages,
1718
} from "../types"
18-
import { createOrLinkApp } from "../ui/pickers"
19+
import { ui } from "../ui/dialogs"
20+
import { createNewApp, pickExistingApp, pickTeam } from "../ui/pickers"
1921

2022
// Exclusion patterns - aligned with fastapi-cloud-cli
2123
// See: https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/src/fastapi_cloud_cli/commands/deploy.py
22-
const EXCLUDE_DIRS = new Set([
24+
const EXCLUDE_PARTS = [
2325
".venv",
2426
"__pycache__",
2527
".mypy_cache",
2628
".pytest_cache",
2729
".git",
28-
])
29-
const EXCLUDE_FILES = new Set([".gitignore", ".fastapicloudignore"])
30+
".gitignore",
31+
".fastapicloudignore",
32+
]
3033

3134
// 300 attempts x 2 seconds = 10 minutes maximum
3235
const MAX_POLL_ATTEMPTS = 300
3336
const DEPLOYMENT_POLL_INTERVAL_MS = 2000
3437

3538
export function shouldExclude(relativePath: string): boolean {
3639
const parts = relativePath.split("/")
37-
const fileName = parts[parts.length - 1]
3840

39-
// Check if any path component is in exclude list
40-
for (const part of parts) {
41-
if (EXCLUDE_DIRS.has(part)) return true
41+
if (parts.some((part) => EXCLUDE_PARTS.includes(part))) {
42+
return true
43+
}
44+
45+
if (relativePath.endsWith(".pyc")) {
46+
return true
4247
}
4348

44-
// Check file-level exclusions
45-
if (EXCLUDE_FILES.has(fileName)) return true
46-
if (fileName.endsWith(".pyc")) return true
49+
const fileName = parts[parts.length - 1]
50+
if (fileName === ".env" || fileName.startsWith(".env.")) {
51+
return true
52+
}
4753

4854
return false
4955
}
@@ -59,7 +65,7 @@ export async function deploy(context: DeployContext): Promise<boolean> {
5965
const { workspaceRoot, configService, apiService, statusBarItem } = context
6066

6167
if (!workspaceRoot) {
62-
vscode.window.showErrorMessage("No workspace folder open")
68+
ui.showErrorMessage("No workspace folder open")
6369
return false
6470
}
6571

@@ -71,7 +77,7 @@ export async function deploy(context: DeployContext): Promise<boolean> {
7177
silent: true,
7278
})
7379
if (!session) {
74-
const result = await vscode.window.showErrorMessage(
80+
const result = await ui.showErrorMessage(
7581
"Please sign in to FastAPI Cloud first.",
7682
"Sign In",
7783
)
@@ -84,11 +90,38 @@ export async function deploy(context: DeployContext): Promise<boolean> {
8490
const existingConfig = await configService.getConfig(workspaceRoot)
8591
const config: Config = existingConfig ?? { app_id: "", team_id: "" }
8692
if (!config.app_id) {
87-
const app = await createOrLinkApp(apiService, workspaceRoot)
93+
const team = await pickTeam(apiService)
94+
if (!team) return false
95+
96+
const choice = await ui.showQuickPick(
97+
[
98+
{
99+
label: "$(link) Link Existing App",
100+
description: "Connect to an app on FastAPI Cloud",
101+
id: "link",
102+
},
103+
{
104+
label: "$(add) Create New App",
105+
description: "Create a new app and link it",
106+
id: "create",
107+
},
108+
],
109+
{ placeHolder: "Set up FastAPI Cloud" },
110+
)
111+
if (!choice) return false
112+
113+
let app: App | null
114+
if (choice.id === "create") {
115+
const folderName = workspaceRoot.path.split("/").pop() || "my-app"
116+
app = await createNewApp(apiService, team, folderName)
117+
} else {
118+
app = await pickExistingApp(apiService, team)
119+
}
88120
if (!app) return false
89-
config.app_id = app.app.id
90-
config.team_id = app.team.id
91-
config.app_slug = app.app.slug
121+
122+
config.app_id = app.id
123+
config.team_id = team.id
124+
config.app_slug = app.slug
92125
await configService.writeConfig(workspaceRoot, config)
93126
}
94127

@@ -116,7 +149,7 @@ export async function deploy(context: DeployContext): Promise<boolean> {
116149
)
117150

118151
if (result) {
119-
const action = await vscode.window.showInformationMessage(
152+
const action = await ui.showInformationMessage(
120153
"Deployed successfully!",
121154
"Open App",
122155
"View Dashboard",

src/cloud/ui/dialogs.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as vscode from "vscode"
2+
3+
/**
4+
* UI dialog wrappers to avoid overload complexity in tests.
5+
* These wrap VS Code's window methods with simpler signatures that are easier to stub.
6+
*/
7+
export const ui = {
8+
showErrorMessage: async (
9+
message: string,
10+
...items: string[]
11+
): Promise<string | undefined> => {
12+
return vscode.window.showErrorMessage(message, ...items)
13+
},
14+
15+
showInformationMessage: async (
16+
message: string,
17+
...items: string[]
18+
): Promise<string | undefined> => {
19+
return vscode.window.showInformationMessage(message, ...items)
20+
},
21+
22+
showQuickPick: async <T extends vscode.QuickPickItem>(
23+
items: readonly T[],
24+
options?: vscode.QuickPickOptions,
25+
): Promise<T | undefined> => {
26+
return vscode.window.showQuickPick(items, options)
27+
},
28+
29+
showWarningMessage: async (
30+
message: string,
31+
options: vscode.MessageOptions,
32+
...items: string[]
33+
): Promise<string | undefined> => {
34+
return vscode.window.showWarningMessage(message, options, ...items)
35+
},
36+
}

src/cloud/ui/menus.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { ApiService } from "../api"
77
import { AUTH_PROVIDER_ID } from "../auth"
88
import type { WorkspaceState } from "../types"
9+
import { ui } from "./dialogs"
910

1011
export interface MenuActions {
1112
signOut: () => Promise<void>
@@ -41,7 +42,7 @@ export class MenuHandler {
4142

4243
const activeFolder = this.getActiveWorkspaceFolder()
4344
if (!activeFolder) {
44-
vscode.window.showErrorMessage("No workspace folder open")
45+
ui.showErrorMessage("No workspace folder open")
4546
return
4647
}
4748

@@ -74,7 +75,7 @@ export class MenuHandler {
7475
},
7576
]
7677

77-
const selected = await vscode.window.showQuickPick(items, {
78+
const selected = await ui.showQuickPick(items, {
7879
placeHolder: "Set up FastAPI Cloud",
7980
})
8081

@@ -110,7 +111,7 @@ export class MenuHandler {
110111
{ label: "$(ellipsis) More", id: "more" },
111112
]
112113

113-
const selected = await vscode.window.showQuickPick(items, {
114+
const selected = await ui.showQuickPick(items, {
114115
placeHolder: app.slug,
115116
})
116117

@@ -150,7 +151,7 @@ export class MenuHandler {
150151
},
151152
]
152153

153-
const selected = await vscode.window.showQuickPick(items, {
154+
const selected = await ui.showQuickPick(items, {
154155
placeHolder: "More options",
155156
})
156157

src/cloud/ui/pickers.ts

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode"
22
import type { ApiService } from "../api"
33
import type { App, Team } from "../types"
4+
import { ui } from "./dialogs"
45

56
/**
67
* Shows a quick pick to select a team. Auto-selects if only one team.
@@ -14,12 +15,12 @@ export async function pickTeam(apiService: ApiService): Promise<Team | null> {
1415
error instanceof Error && error.message === "Not authenticated"
1516
? "Please sign in to FastAPI Cloud first."
1617
: "Failed to fetch teams. Please check your connection."
17-
vscode.window.showErrorMessage(message)
18+
ui.showErrorMessage(message)
1819
return null
1920
}
2021

2122
if (teams.length === 0) {
22-
vscode.window.showErrorMessage(
23+
ui.showErrorMessage(
2324
"No teams found. Please create a team on FastAPI Cloud first.",
2425
)
2526
return null
@@ -30,7 +31,7 @@ export async function pickTeam(apiService: ApiService): Promise<Team | null> {
3031
}
3132

3233
const teamItems = teams.map((t) => ({ label: t.name, team: t }))
33-
const picked = await vscode.window.showQuickPick(teamItems, {
34+
const picked = await ui.showQuickPick(teamItems, {
3435
placeHolder: "Select a team",
3536
})
3637

@@ -48,14 +49,12 @@ export async function pickExistingApp(
4849
try {
4950
apps = await apiService.getApps(team.id)
5051
} catch {
51-
vscode.window.showErrorMessage(
52-
"Failed to fetch apps. Please check your connection.",
53-
)
52+
ui.showErrorMessage("Failed to fetch apps. Please check your connection.")
5453
return null
5554
}
5655

5756
if (apps.length === 0) {
58-
vscode.window.showErrorMessage(
57+
ui.showErrorMessage(
5958
"No apps found for this team. Please create an app on FastAPI Cloud first.",
6059
)
6160
return null
@@ -66,7 +65,7 @@ export async function pickExistingApp(
6665
description: a.url,
6766
app: a,
6867
}))
69-
const picked = await vscode.window.showQuickPick(appItems, {
68+
const picked = await ui.showQuickPick(appItems, {
7069
placeHolder: "Select an app",
7170
})
7271

@@ -97,48 +96,12 @@ export async function createNewApp(
9796

9897
try {
9998
const app = await apiService.createApp(team.id, appName)
100-
vscode.window.showInformationMessage(`Created app: ${app.slug}`)
99+
ui.showInformationMessage(`Created app: ${app.slug}`)
101100
return app
102101
} catch (error) {
103-
vscode.window.showErrorMessage(
102+
ui.showErrorMessage(
104103
`Failed to create app: ${error instanceof Error ? error.message : "Unknown error"}`,
105104
)
106105
return null
107106
}
108107
}
109-
110-
/**
111-
* Shows picker to create or link an app for deployment.
112-
*/
113-
export async function createOrLinkApp(
114-
apiService: ApiService,
115-
workspaceRoot: vscode.Uri,
116-
): Promise<{ app: App; team: Team } | null> {
117-
const team = await pickTeam(apiService)
118-
if (!team) return null
119-
120-
const choice = await vscode.window.showQuickPick([
121-
{
122-
label: "$(link) Link Existing App",
123-
description: "Connect to an app on FastAPI Cloud",
124-
id: "link",
125-
},
126-
{
127-
label: "$(add) Create New App",
128-
description: "Create a new app and link it",
129-
id: "create",
130-
},
131-
])
132-
133-
if (!choice) return null
134-
135-
let app: App | null = null
136-
if (choice.id === "create") {
137-
const folderName = workspaceRoot.path.split("/").pop() || "my-app"
138-
app = await createNewApp(apiService, team, folderName)
139-
} else if (choice.id === "link") {
140-
app = await pickExistingApp(apiService, team)
141-
}
142-
if (!app) return null
143-
return { app, team }
144-
}

src/test/cloud/commands/auth.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as assert from "node:assert"
22
import sinon from "sinon"
3-
import * as vscode from "vscode"
43
import { signOut } from "../../../cloud/commands/auth"
54
import type { AuthProvider } from "../../../cloud/types"
5+
import { ui } from "../../../cloud/ui/dialogs"
66

77
suite("cloud/commands/auth", () => {
88
teardown(() => sinon.restore())
@@ -13,9 +13,7 @@ suite("cloud/commands/auth", () => {
1313
signOut: sinon.stub().resolves(),
1414
} as unknown as AuthProvider
1515

16-
sinon
17-
.stub(vscode.window, "showWarningMessage")
18-
.resolves("Sign Out" as any)
16+
sinon.stub(ui, "showWarningMessage").resolves("Sign Out")
1917

2018
const result = await signOut(authProvider)
2119

@@ -28,7 +26,7 @@ suite("cloud/commands/auth", () => {
2826
signOut: sinon.stub().resolves(),
2927
} as unknown as AuthProvider
3028

31-
sinon.stub(vscode.window, "showWarningMessage").resolves(undefined as any)
29+
sinon.stub(ui, "showWarningMessage").resolves(undefined)
3230

3331
const result = await signOut(authProvider)
3432

0 commit comments

Comments
 (0)