Skip to content

Commit 16ca9f4

Browse files
authored
Handle case where user is already logged into another deployment (#11)
* Fix util cleanup not waiting Also extracted the common string into a constant * Extract common zip temp path constant * Move vscode-related code out of utils This lets it be shared by code that runs outside VS Code (like test setup). * Update Node types The previous types did not have definitions for `assert.match`. Remove "preferably" from the readme. You really should use 16 or greater otherwise it does not generate the same lockfile. * Mock config file location in tests This lets us read/write in tests without messing with the real configuration. * Check if logged in another deployment * Replace manual env handling with new set/resetEnv * Bump version to 0.0.5
1 parent 42ce30d commit 16ca9f4

15 files changed

+196
-57
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Development
88

9-
- Install Node (preferably >= 16)
9+
- Install Node >= 16
1010
- Clone the repository locally
1111
- Run `npm install`
1212

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"description": "Connect VS Code to your Coder Workspaces",
66
"repository": "https://github.com/cdr/vscode-coder",
77
"preview": true,
8-
"version": "0.0.4",
8+
"version": "0.0.5",
99
"engines": {
1010
"vscode": "^1.54.0"
1111
},
@@ -159,7 +159,7 @@
159159
"@types/adm-zip": "^0.4.34",
160160
"@types/glob": "^7.1.3",
161161
"@types/mocha": "^8.0.4",
162-
"@types/node": "^12.11.7",
162+
"@types/node": "^16.11.21",
163163
"@types/tar-fs": "^2.0.1",
164164
"@types/vscode": "^1.54.0",
165165
"@types/which": "^2.0.1",

src/auth.test.ts

+45-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,51 @@
1-
// import * as assert from "assert"
1+
import * as assert from "assert"
2+
import { promises as fs } from "fs"
3+
import * as path from "path"
24
import * as vscode from "vscode"
3-
// import * as auth from "./download"
5+
import * as auth from "./auth"
6+
import * as utils from "./utils"
47

58
suite("Authenticate", () => {
69
vscode.window.showInformationMessage("Start authenticate tests.")
710

8-
// TODO: Implement.
9-
test("authenticate")
11+
const tmpPath = "tests/auth"
12+
suiteSetup(async () => {
13+
// Cleanup anything left over from the last run.
14+
await utils.clean(tmpPath)
15+
})
16+
17+
teardown(() => utils.resetEnv())
18+
19+
const assertDirs = (dir: string) => {
20+
assert.match(auth.getConfigDir("linux"), new RegExp(path.join(dir, ".config$")))
21+
assert.match(auth.getConfigDir("freebsd"), new RegExp(path.join(dir, ".config$")))
22+
assert.match(auth.getConfigDir("win32"), new RegExp(path.join(dir, "AppData/Roaming$")))
23+
assert.match(auth.getConfigDir("darwin"), new RegExp(path.join(dir, "Library/Application Support$")))
24+
}
25+
26+
test("getConfigDir", async () => {
27+
// Make sure local config mocks work.
28+
const tmpDir = await utils.tmpdir(tmpPath)
29+
utils.setEnv("HOME", tmpDir)
30+
assertDirs(tmpDir)
31+
32+
// Make sure the global mock also works. For example the Linux temp config
33+
// directory looks like: /tmp/coder/tests/config/tmp-Dzfqwl/home/.config
34+
// This runs after the local mock to make sure environment variables are
35+
// being restored correctly.
36+
utils.resetEnv()
37+
assertDirs("tests/config/.+/home")
38+
})
39+
40+
test("currentUri", async () => {
41+
const tmpDir = await utils.tmpdir(tmpPath)
42+
utils.setEnv("HOME", tmpDir)
43+
44+
const accessUri = "https://coder-workspaces-test"
45+
assert.strictEqual(await auth.currentUri(), undefined)
46+
const dir = path.join(auth.getConfigDir(), "coder")
47+
await fs.mkdir(dir, { recursive: true })
48+
await fs.writeFile(path.join(dir, "url"), accessUri)
49+
assert.strictEqual(await auth.currentUri(), accessUri)
50+
})
1051
})

src/auth.ts

+23-5
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@ import { promises as fs } from "fs"
22
import * as os from "os"
33
import * as path from "path"
44
import * as vscode from "vscode"
5-
import { debug } from "./utils"
5+
import { debug } from "./logs"
66

7-
const getConfigDir = (): string => {
7+
export const getConfigDir = (platform = process.platform): string => {
88
// The CLI uses localConfig from https://github.com/kirsle/configdir.
9-
switch (process.platform) {
9+
switch (platform) {
1010
case "win32":
1111
return process.env.APPDATA || path.join(os.homedir(), "AppData/Roaming")
1212
case "darwin":
1313
return path.join(os.homedir(), "Library/Application Support")
14-
case "linux":
14+
default:
1515
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
1616
}
17-
throw new Error(`Unsupported platform ${process.platform}`)
1817
}
1918

2019
/**
@@ -58,6 +57,25 @@ const doAuthenticate = async (accessUrl?: string, token?: string): Promise<void>
5857
await Promise.all([fs.writeFile(path.join(dir, "session"), token), fs.writeFile(path.join(dir, "url"), accessUrl)])
5958
}
6059

60+
/**
61+
* Return current login URI, if any.
62+
*/
63+
export const currentUri = async (): Promise<string | undefined> => {
64+
// TODO: Like authentication this unfortunately relies on internal knowledge
65+
// since there does not appear to be a command to get the current login
66+
// status.
67+
const dir = path.join(getConfigDir(), "coder")
68+
try {
69+
return (await fs.readFile(path.join(dir, "url"), "utf8")).trim()
70+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71+
} catch (error: any) {
72+
if (error.code === "ENOENT") {
73+
return undefined
74+
}
75+
throw error
76+
}
77+
}
78+
6179
/** Only allow one at a time. */
6280
let promise: Promise<void> | undefined
6381

src/download.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { promises as fs } from "fs"
22
import * as path from "path"
33
import * as vscode from "vscode"
4+
import { debug } from "./logs"
45
import { requestResponse } from "./request"
5-
import { debug, extractTar, extractZip, getAssetUrl } from "./utils"
6+
import { extractTar, extractZip, getAssetUrl } from "./utils"
67

78
/**
89
* Inner function for `download` so it can wrap with a singleton promise.

src/exec.test.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,25 @@ import * as assert from "assert"
22
import * as cp from "child_process"
33
import * as vscode from "vscode"
44
import * as exec from "./exec"
5+
import * as utils from "./utils"
56

67
suite("Exec", () => {
78
vscode.window.showInformationMessage("Start exec tests.")
89

9-
teardown(() => {
10-
delete process.env.CODER_MOCK_STATE
11-
})
10+
teardown(() => utils.resetEnv())
1211

1312
test("execCoder", async () => {
1413
assert.strictEqual(await exec.execCoder("--help"), "help\n")
1514

1615
// This will attempt to authenticate first, which will fail.
17-
process.env.CODER_MOCK_STATE = "fail"
16+
utils.setEnv("CODER_MOCK_STATE", "fail")
1817
await assert.rejects(exec.execCoder("--help"), {
1918
name: "Error",
2019
message: /Command failed: .+ --help\nstderr message from fail state\n/,
2120
})
21+
22+
// TODO: Test what happens when you are already logged in once we figure out
23+
// how to test notifications and user input.
2224
})
2325

2426
test("onLine", async () => {

src/exec.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,25 @@ import * as cp from "child_process"
22
import * as path from "path"
33
import * as stream from "stream"
44
import { promisify } from "util"
5+
import * as vscode from "vscode"
56
import * as nodeWhich from "which"
6-
import { authenticate } from "./auth"
7+
import { authenticate, currentUri } from "./auth"
78
import { download } from "./download"
8-
import { context, debug } from "./utils"
9+
import { debug } from "./logs"
10+
11+
let _context: vscode.ExtensionContext | undefined
12+
13+
/**
14+
* Get or set the extension context.
15+
*/
16+
export const context = (ctx?: vscode.ExtensionContext): vscode.ExtensionContext => {
17+
if (ctx) {
18+
_context = ctx
19+
} else if (!_context) {
20+
throw new Error("Context has not been set; has the extension been activated?")
21+
}
22+
return _context
23+
}
924

1025
/**
1126
* How to invoke the Coder CLI.
@@ -52,6 +67,27 @@ export const execCoder = async (command: string, opts?: CoderOptions): Promise<s
5267
throw new Error(`${error.message.trim()}. Please [install manually](https://coder.com/docs/cli/installation).`)
5368
}
5469

70+
if (opts?.accessUri) {
71+
const uri = await currentUri()
72+
if (!uri) {
73+
// Not authenticated to anything.
74+
await authenticate(opts?.accessUri, opts?.token)
75+
} else if (opts?.accessUri !== uri) {
76+
// Authenticated to a different deployment.
77+
const target = opts?.accessUri.replace(/https?:\/\//, "")
78+
const current = uri.replace(/https?:\/\//, "")
79+
const action = await vscode.window.showInformationMessage(
80+
`This workspace is hosted at ${target} but you are already logged into ${current}. Logging into ${target} will log you out of ${current}.`,
81+
"Cancel",
82+
`Log into ${target}`,
83+
)
84+
if (!action || action === "Cancel") {
85+
throw new Error("Login canceled")
86+
}
87+
await authenticate(opts?.accessUri, opts?.token)
88+
}
89+
}
90+
5591
try {
5692
const output = await promisify(cp.exec)(coderBinary + " " + command)
5793
return output.stdout

src/extension.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22

33
import * as qs from "querystring"
44
import * as vscode from "vscode"
5+
import { context } from "./exec"
56
import { CoderHelpProvider } from "./help"
6-
77
import {
88
coderWorkspaceInspectDocumentProvider,
99
coderWorkspaceLogsDocumentProvider,
10+
debug,
1011
handleInspectCommand,
1112
handleShowLogsCommand,
1213
} from "./logs"
13-
import { context, debug, getQueryValue, split } from "./utils"
14+
import { getQueryValue, split } from "./utils"
1415
import {
1516
CoderWorkspacesProvider,
1617
rebuildWorkspace,

src/install.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as path from "path"
33
import * as vscode from "vscode"
44
import { download } from "./download"
55
import { binaryExists, onLine, wrapExit } from "./exec"
6-
import { outputChannel } from "./utils"
6+
import { outputChannel } from "./logs"
77

88
/**
99
* Install the Coder CLI using the provided command.

src/logs.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// import * as assert from "assert"
2+
import * as vscode from "vscode"
3+
// import * as logs from "./logs"
4+
5+
suite("Logs", () => {
6+
vscode.window.showInformationMessage("Start log tests.")
7+
8+
// TODO: Implement.
9+
test("debug")
10+
})

src/logs.ts

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import * as yaml from "yaml"
33
import { execCoder } from "./exec"
44
import { CoderWorkspace } from "./workspaces"
55

6+
export const outputChannel = vscode.window.createOutputChannel("Coder")
7+
8+
export const debug = (line: string): void => {
9+
if (process.env.CODER_DEBUG) {
10+
outputChannel.appendLine(line)
11+
}
12+
}
13+
614
export const handleShowLogsCommand = async ({ workspace }: { workspace: CoderWorkspace }): Promise<void> => {
715
const uri = vscode.Uri.parse("coder-logs:" + workspace.name)
816
const doc = await vscode.workspace.openTextDocument(uri)

src/test/runTest.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { runTests } from "@vscode/test-electron"
22
import * as path from "path"
3+
import * as utils from "../utils"
34

45
// Place the mock binary into PATH.
56
process.env.PATH = `${path.resolve(__dirname, "../../fixtures")}${path.delimiter}${process.env.PATH}`
@@ -14,6 +15,12 @@ if (process.platform === "win32") {
1415

1516
async function main() {
1617
try {
18+
// Cleanup anything left over from the last run.
19+
const tmpPath = "tests/config"
20+
await utils.clean(tmpPath)
21+
const temp = await utils.tmpdir(tmpPath)
22+
process.env.HOME = path.join(temp, "home")
23+
1724
// The folder containing the Extension Manifest package.json
1825
// Passed to `--extensionDevelopmentPath`
1926
const extensionDevelopmentPath = path.resolve(__dirname, "../..")

src/utils.test.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import * as utils from "./utils"
77
suite("Utils", () => {
88
vscode.window.showInformationMessage("Start util tests.")
99

10-
suiteSetup(() => {
10+
const tmpPath = "tests/utils"
11+
suiteSetup(async () => {
1112
// Cleanup anything left over from the last run.
12-
utils.clean("tests/utils")
13+
await utils.clean(tmpPath)
1314
})
1415

1516
test("split", () => {
@@ -20,7 +21,7 @@ suite("Utils", () => {
2021

2122
test("extract", async () => {
2223
for (const ext of [".tar.gz", ".zip"]) {
23-
const temp = await utils.tmpdir("tests/utils")
24+
const temp = await utils.tmpdir(tmpPath)
2425
const stream = fs.createReadStream(path.resolve(__dirname, `../fixtures/archive${ext}`))
2526

2627
await (ext === ".tar.gz" ? utils.extractTar(stream, temp) : utils.extractZip(stream, temp))
@@ -48,4 +49,13 @@ suite("Utils", () => {
4849
assert.strictEqual(utils.getQueryValue(["bar"]), "bar")
4950
assert.strictEqual(utils.getQueryValue(["bazzle", "qux"]), "bazzle")
5051
})
52+
53+
test("set/resetEnv", () => {
54+
const key = "CODER_WORKSPACES_FOO"
55+
assert.strictEqual(process.env[key], undefined)
56+
utils.setEnv(key, "baz")
57+
assert.strictEqual(process.env[key], "baz")
58+
utils.resetEnv()
59+
assert.strictEqual(process.env[key], undefined)
60+
})
5161
})

0 commit comments

Comments
 (0)