Merge branch 'master' into upgrade-vscode-1.53

This commit is contained in:
Joe Previte 2021-02-26 14:23:24 -07:00 committed by GitHub
commit 9ea18636d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 338 additions and 18 deletions

2
.github/CODEOWNERS vendored
View File

@ -1 +1,3 @@
* @cdr/code-server-reviewers
ci/helm-chart @Matthew-Beckett @alexgorbatchev ci/helm-chart @Matthew-Beckett @alexgorbatchev

View File

@ -24,6 +24,9 @@ jobs:
test: test:
needs: linux-amd64 needs: linux-amd64
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
PASSWORD: e45432jklfdsab
CODE_SERVER_ADDRESS: http://localhost:8080
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Download release packages - name: Download release packages
@ -37,9 +40,14 @@ jobs:
- uses: microsoft/playwright-github-action@v1 - uses: microsoft/playwright-github-action@v1
- name: Install dependencies and run tests - name: Install dependencies and run tests
run: | run: |
node ./release-packages/code-server*-linux-amd64 & ./release-packages/code-server*-linux-amd64/bin/code-server --home $CODE_SERVER_ADDRESS/healthz &
yarn --frozen-lockfile yarn --frozen-lockfile
yarn test yarn test
- name: Upload test artifacts
uses: actions/upload-artifact@v2
with:
name: test-videos
path: ./test/videos
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest

2
.gitignore vendored
View File

@ -16,3 +16,5 @@ node-*
.home .home
coverage coverage
**/.DS_Store **/.DS_Store
test/videos
test/screenshots

View File

@ -43,8 +43,9 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
9. Update the AUR package. 9. Update the AUR package.
- Instructions on updating the AUR package are at [cdr/code-server-aur](https://github.com/cdr/code-server-aur). - Instructions on updating the AUR package are at [cdr/code-server-aur](https://github.com/cdr/code-server-aur).
10. Wait for the npm package to be published. 10. Wait for the npm package to be published.
11. Update the homebrew package. 11. Update the [homebrew package](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb).
- Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated. 1. Install [homebrew](https://brew.sh/)
2. Run `brew bump-formula-pr --version=3.8.1 code-server` and update the version accordingly. This will bump the version and open a PR. Note: this will only work once the version is published on npm.
## Updating Code Coverage in README ## Updating Code Coverage in README

View File

@ -9,6 +9,18 @@ main() {
# information. We must also run it from the root otherwise coverage will not # information. We must also run it from the root otherwise coverage will not
# include our source files. # include our source files.
cd "$OLDPWD" cd "$OLDPWD"
if [[ -z ${PASSWORD-} ]] || [[ -z ${CODE_SERVER_ADDRESS-} ]]; then
echo "The end-to-end testing suites rely on your local environment"
echo -e "\n"
echo "Please set the following environment variables locally:"
echo " \$PASSWORD"
echo " \$CODE_SERVER_ADDRESS"
echo -e "\n"
echo "Please make sure you have code-server running locally with the flag:"
echo " --home \$CODE_SERVER_ADDRESS/healthz "
echo -e "\n"
exit 1
fi
CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@"
} }

View File

@ -35,8 +35,9 @@ There are several differences, however. You must:
- Use Node.js version 12.x (or greater) - Use Node.js version 12.x (or greater)
- Have [yarn](https://classic.yarnpkg.com/en/) installed (which is used to install JS packages and run development scripts) - Have [yarn](https://classic.yarnpkg.com/en/) installed (which is used to install JS packages and run development scripts)
- Have [nfpm](https://github.com/goreleaser/nfpm) (which is used to build `.deb` and `.rpm` packages and [jq](https://stedolan.github.io/jq/) (used to build code-server releases) installed - Have [nfpm](https://github.com/goreleaser/nfpm) (which is used to build `.deb` and `.rpm` packages and [jq](https://stedolan.github.io/jq/) (used to build code-server releases) installed
- Have [shfmt](https://pkg.go.dev/mvdan.cc/sh/v3) installed to run `yarn fmt` (requires Go is installed on your system)
The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all The [CI container](../ci/images/debian10/Dockerfile) is a useful reference for all
of the dependencies code-server uses. of the dependencies code-server uses.
## Development Workflow ## Development Workflow
@ -120,10 +121,10 @@ node ./release
The `code-server` script serves an HTTP API for login and starting a remote VS Code process. The `code-server` script serves an HTTP API for login and starting a remote VS Code process.
The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in The CLI code is in [src/node](../src/node) and the HTTP routes are implemented in
[./src/node/app](./src/node/app). [src/node/routes](../src/node/routes).
Most of the meaty parts are in the VS Code portion of the codebase under [./lib/vscode](./lib/vscode), which we described next. Most of the meaty parts are in the VS Code portion of the codebase under [lib/vscode](../lib/vscode), which we described next.
### Modifications to VS Code ### Modifications to VS Code
@ -133,7 +134,7 @@ and exposed an API to the front-end for file access and all UI needs.
Over time, Microsoft added support to VS Code to run it on the web. They have made Over time, Microsoft added support to VS Code to run it on the web. They have made
the front-end open source, but not the server. As such, code-server v2 (and later) uses the front-end open source, but not the server. As such, code-server v2 (and later) uses
the VS Code front-end and implements the server. We do this by using a git subtree to fork and modify VS Code. This code lives under [./lib/vscode](./lib/vscode). the VS Code front-end and implements the server. We do this by using a git subtree to fork and modify VS Code. This code lives under [lib/vscode](../lib/vscode).
Some noteworthy changes in our version of VS Code: Some noteworthy changes in our version of VS Code:

View File

@ -4,7 +4,7 @@
- [Questions?](#questions) - [Questions?](#questions)
- [iPad Status?](#ipad-status) - [iPad Status?](#ipad-status)
- [Community projects (awesome-code-server)](#community-projects-awesome-code-server) - [Community Projects (awesome-code-server)](#community-projects-awesome-code-server)
- [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration) - [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration)
- [Differences compared to VS Code?](#differences-compared-to-vs-code) - [Differences compared to VS Code?](#differences-compared-to-vs-code)
- [How can I request a missing extension?](#how-can-i-request-a-missing-extension) - [How can I request a missing extension?](#how-can-i-request-a-missing-extension)
@ -43,7 +43,7 @@ Please file all questions and support requests at https://github.com/cdr/code-se
Please see [./ipad.md](./ipad.md). Please see [./ipad.md](./ipad.md).
## Community projects (awesome-code-server) ## Community Projects (awesome-code-server)
Visit the [awesome-code-server](https://github.com/cdr/awesome-code-server) repository to view community projects and guides with code-server! Feel free to add your own! Visit the [awesome-code-server](https://github.com/cdr/awesome-code-server) repository to view community projects and guides with code-server! Feel free to add your own!

View File

@ -143,8 +143,16 @@
"lines": 40 "lines": 40
} }
}, },
"testTimeout": 30000,
"globalSetup": "<rootDir>/test/globalSetup.ts",
"modulePathIgnorePatterns": [ "modulePathIgnorePatterns": [
"<rootDir>/release" "<rootDir>/lib/vscode",
"<rootDir>/release-packages",
"<rootDir>/release",
"<rootDir>/release-standalone",
"<rootDir>/release-npm-package",
"<rootDir>/release-gcp",
"<rootDir>/release-images"
] ]
} }
} }

View File

@ -1,4 +1,6 @@
body { body {
min-height: 568px;
min-width: 320px;
overflow: auto; overflow: auto;
} }
@ -15,6 +17,12 @@ body {
width: 100%; width: 100%;
} }
@media (max-width: 600px) {
.login-form > .field {
flex-direction: column;
}
}
.login-form > .error { .login-form > .error {
color: red; color: red;
margin-top: 16px; margin-top: 16px;
@ -38,6 +46,13 @@ body {
margin-left: 20px; margin-left: 20px;
} }
@media (max-width: 600px) {
.login-form > .field > .submit {
margin-left: 0px;
margin-top: 16px;
}
}
input { input {
-webkit-appearance: none; -webkit-appearance: none;
} }

View File

@ -7,7 +7,7 @@ import { rootPath } from "../constants"
import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http" import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http"
import { hash, humanPath } from "../util" import { hash, humanPath } from "../util"
enum Cookie { export enum Cookie {
Key = "key", Key = "key",
} }

3
test/constants.ts Normal file
View File

@ -0,0 +1,3 @@
export const CODE_SERVER_ADDRESS = process.env.CODE_SERVER_ADDRESS || "http://localhost:8080"
export const PASSWORD = process.env.PASSWORD || "e45432jklfdsab"
export const STORAGE = process.env.STORAGE || ""

View File

@ -1,4 +1,5 @@
import { chromium, Page, Browser } from "playwright" import { chromium, Page, Browser } from "playwright"
import { CODE_SERVER_ADDRESS } from "./constants"
let browser: Browser let browser: Browser
let page: Page let page: Page
@ -17,7 +18,7 @@ afterEach(async () => {
}) })
it("should see the login page", async () => { it("should see the login page", async () => {
await page.goto("http://localhost:8080") await page.goto(CODE_SERVER_ADDRESS)
// It should send us to the login page // It should send us to the login page
expect(await page.title()).toBe("code-server login") expect(await page.title()).toBe("code-server login")
}) })

34
test/globalSetup.ts Normal file
View File

@ -0,0 +1,34 @@
// This setup runs before our e2e tests
// so that it authenticates us into code-server
// ensuring that we're logged in before we run any tests
import { chromium } from "playwright"
import { CODE_SERVER_ADDRESS, PASSWORD } from "./constants"
import * as wtfnode from "./wtfnode"
module.exports = async () => {
console.log("\n🚨 Running Global Setup for Jest Tests")
console.log(" Please hang tight...")
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()
if (process.env.WTF_NODE) {
wtfnode.setup()
}
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "domcontentloaded" })
// Type in password
await page.fill(".password", PASSWORD)
// Click the submit button and login
await page.click(".submit")
// Save storage state and store as an env variable
// More info: https://playwright.dev/docs/auth?_highlight=authe#reuse-authentication-state
const storage = await context.storageState()
process.env.STORAGE = JSON.stringify(storage)
await page.close()
await browser.close()
await context.close()
console.log("✅ Global Setup for Jest Tests is now complete.")
}

88
test/goHome.test.ts Normal file
View File

@ -0,0 +1,88 @@
import { chromium, Page, Browser, BrowserContext, Cookie } from "playwright"
import { hash } from "../src/node/util"
import { CODE_SERVER_ADDRESS, PASSWORD, STORAGE } from "./constants"
import { createCookieIfDoesntExist } from "./helpers"
describe("go home", () => {
let browser: Browser
let page: Page
let context: BrowserContext
beforeAll(async () => {
browser = await chromium.launch()
// Create a new context with the saved storage state
const storageState = JSON.parse(STORAGE) || {}
const cookieToStore = {
sameSite: "Lax" as const,
name: "key",
value: hash(PASSWORD),
domain: "localhost",
path: "/",
expires: -1,
httpOnly: false,
secure: false,
}
// For some odd reason, the login method used in globalSetup.ts doesn't always work
// I don't know if it's on playwright clearing our cookies by accident
// or if it's our cookies disappearing.
// This means we need an additional check to make sure we're logged in.
// We do this by manually adding the cookie to the browser environment
// if it's not there at the time the test starts
const cookies: Cookie[] = storageState.cookies || []
// If the cookie exists in cookies then
// this will return the cookies with no changes
// otherwise if it doesn't exist, it will create it
// hence the name maybeUpdatedCookies
//
// TODO(@jsjoeio)
// The playwright storage thing sometimes works and sometimes doesn't. We should investigate this further
// at some point.
// See discussion: https://github.com/cdr/code-server/pull/2648#discussion_r575434946
const maybeUpdatedCookies = createCookieIfDoesntExist(cookies, cookieToStore)
context = await browser.newContext({
storageState: { cookies: maybeUpdatedCookies },
recordVideo: { dir: "./test/videos/" },
})
})
afterAll(async () => {
// Remove password from local storage
await context.clearCookies()
await context.close()
await browser.close()
})
beforeEach(async () => {
page = await context.newPage()
})
// NOTE: this test will fail if you do not run code-server with --home $CODE_SERVER_ADDRESS/healthz
it("should see a 'Go Home' button in the Application Menu that goes to /healthz", async () => {
const GO_HOME_URL = `${CODE_SERVER_ADDRESS}/healthz`
// Sometimes a dialog shows up when you navigate
// asking if you're sure you want to leave
// so we listen if it comes, we accept it
page.on("dialog", (dialog) => dialog.accept())
// waitUntil: "domcontentloaded"
// In case the page takes a long time to load
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "domcontentloaded" })
// Click the Home menu
await page.click(".home-bar ul[aria-label='Home'] li")
// See the Go Home button
const goHomeButton = "a.action-menu-item span[aria-label='Go Home']"
expect(await page.isVisible(goHomeButton))
// Click it and navigate to /healthz
// NOTE: ran into issues of it failing intermittently
// without having button: "middle"
await Promise.all([page.waitForNavigation(), page.click(goHomeButton, { button: "middle" })])
expect(page.url()).toBe(GO_HOME_URL)
})
})

35
test/helpers.ts Normal file
View File

@ -0,0 +1,35 @@
// Borrowed from playwright
export interface Cookie {
name: string
value: string
domain: string
path: string
/**
* Unix time in seconds.
*/
expires: number
httpOnly: boolean
secure: boolean
sameSite: "Strict" | "Lax" | "None"
}
/**
* Checks if a cookie exists in array of cookies
*/
export function checkForCookie(cookies: Array<Cookie>, key: string): boolean {
// Check for a cookie where the name is equal to key
return Boolean(cookies.find((cookie) => cookie.name === key))
}
/**
* Creates a login cookie if one doesn't already exist
*/
export function createCookieIfDoesntExist(cookies: Array<Cookie>, cookieToStore: Cookie): Array<Cookie> {
const cookieName = cookieToStore.name
const doesCookieExist = checkForCookie(cookies, cookieName)
if (!doesCookieExist) {
const updatedCookies = [...cookies, cookieToStore]
return updatedCookies
}
return cookies
}

38
test/login.test.ts Normal file
View File

@ -0,0 +1,38 @@
import { chromium, Page, Browser, BrowserContext } from "playwright"
import { CODE_SERVER_ADDRESS, PASSWORD } from "./constants"
describe("login", () => {
let browser: Browser
let page: Page
let context: BrowserContext
beforeAll(async () => {
browser = await chromium.launch()
context = await browser.newContext()
})
afterAll(async () => {
await browser.close()
})
beforeEach(async () => {
page = await context.newPage()
})
afterEach(async () => {
await page.close()
// Remove password from local storage
await context.clearCookies()
})
it("should be able to login", async () => {
await page.goto(CODE_SERVER_ADDRESS)
// Type in password
await page.fill(".password", PASSWORD)
// Click the submit button and login
await page.click(".submit")
// See the editor
const codeServerEditor = await page.isVisible(".monaco-workbench")
expect(codeServerEditor).toBeTruthy()
})
})

View File

@ -6,11 +6,8 @@ import * as tls from "tls"
import { Emitter } from "../src/common/emitter" import { Emitter } from "../src/common/emitter"
import { SocketProxyProvider } from "../src/node/socket" import { SocketProxyProvider } from "../src/node/socket"
import { generateCertificate, tmpdir } from "../src/node/util" import { generateCertificate, tmpdir } from "../src/node/util"
import * as wtfnode from "./wtfnode"
describe("SocketProxyProvider", () => { describe("SocketProxyProvider", () => {
wtfnode.setup()
const provider = new SocketProxyProvider() const provider = new SocketProxyProvider()
const onServerError = new Emitter<{ event: string; error: Error }>() const onServerError = new Emitter<{ event: string; error: Error }>()

View File

@ -1,4 +1,5 @@
import { JSDOM } from "jsdom" import { JSDOM } from "jsdom"
import { Cookie } from "playwright"
// Note: we need to import logger from the root // Note: we need to import logger from the root
// because this is the logger used in logError in ../src/common/util // because this is the logger used in logError in ../src/common/util
import { logger } from "../node_modules/@coder/logger" import { logger } from "../node_modules/@coder/logger"
@ -8,12 +9,16 @@ import {
getFirstString, getFirstString,
getOptions, getOptions,
logError, logError,
normalize,
plural, plural,
resolveBase, resolveBase,
split, split,
trimSlashes, trimSlashes,
normalize,
} from "../src/common/util" } from "../src/common/util"
import { Cookie as CookieEnum } from "../src/node/routes/login"
import { hash } from "../src/node/util"
import { PASSWORD } from "./constants"
import { checkForCookie, createCookieIfDoesntExist } from "./helpers"
const dom = new JSDOM() const dom = new JSDOM()
global.document = dom.window.document global.document = dom.window.document
@ -255,4 +260,58 @@ describe("util", () => {
expect(spy).toHaveBeenCalledWith("api: oh no") expect(spy).toHaveBeenCalledWith("api: oh no")
}) })
}) })
describe("checkForCookie", () => {
it("should check if the cookie exists and has a value", () => {
const fakeCookies: Cookie[] = [
{
name: CookieEnum.Key,
value: hash(PASSWORD),
domain: "localhost",
secure: false,
sameSite: "Lax",
httpOnly: false,
expires: 18000,
path: "/",
},
]
expect(checkForCookie(fakeCookies, CookieEnum.Key)).toBe(true)
})
it("should return false if there are no cookies", () => {
const fakeCookies: Cookie[] = []
expect(checkForCookie(fakeCookies, "key")).toBe(false)
})
})
describe("createCookieIfDoesntExist", () => {
it("should create a cookie if it doesn't exist", () => {
const cookies: Cookie[] = []
const cookieToStore = {
name: CookieEnum.Key,
value: hash(PASSWORD),
domain: "localhost",
secure: false,
sameSite: "Lax" as const,
httpOnly: false,
expires: 18000,
path: "/",
}
expect(createCookieIfDoesntExist(cookies, cookieToStore)).toStrictEqual([cookieToStore])
})
it("should return the same cookies if the cookie already exists", () => {
const PASSWORD = "123supersecure"
const cookieToStore = {
name: CookieEnum.Key,
value: hash(PASSWORD),
domain: "localhost",
secure: false,
sameSite: "Lax" as const,
httpOnly: false,
expires: 18000,
path: "/",
}
const cookies: Cookie[] = [cookieToStore]
expect(createCookieIfDoesntExist(cookies, cookieToStore)).toStrictEqual(cookies)
})
})
}) })

View File

@ -1,7 +1,23 @@
import * as util from "util"
import * as wtfnode from "wtfnode" import * as wtfnode from "wtfnode"
// Jest seems to hijack console.log in a way that makes the output difficult to
// read. So we'll write directly to process.stderr instead.
const write = (...args: [any, ...any]) => {
if (args.length > 0) {
process.stderr.write(util.format(...args) + "\n")
}
}
wtfnode.setLogger("info", write)
wtfnode.setLogger("warn", write)
wtfnode.setLogger("error", write)
let active = false let active = false
/**
* Start logging open handles periodically. This can be used to see what is
* hanging open if anything.
*/
export function setup(): void { export function setup(): void {
if (active) { if (active) {
return return