From fabc4720e3ae7d727db300a4a7108da0e8c283e4 Mon Sep 17 00:00:00 2001 From: Thorarinn Sigurdsson Date: Mon, 30 Mar 2020 10:37:58 +0200 Subject: [PATCH] feat(platform): add client login to CLI * Added login and logout commands (not included in the docs for now) * Auth logic for reading and storing client auth tokens (we use the local SQLite DB for this). * Auth logic for visiting the platform's login page, and for receiving redirects from that page containing a valid client token. --- garden-service/package-lock.json | 60 +++--- garden-service/package.json | 3 +- garden-service/src/cli/cli.ts | 5 +- garden-service/src/commands/commands.ts | 4 + garden-service/src/commands/login.ts | 32 ++++ garden-service/src/commands/logout.ts | 30 +++ garden-service/src/db/base-entity.ts | 8 + garden-service/src/db/connection.ts | 5 +- .../src/db/entities/client-auth-token.ts | 17 ++ garden-service/src/garden.ts | 39 +++- garden-service/src/platform/auth.ts | 177 ++++++++++++++++++ garden-service/test/unit/src/platform/auth.ts | 95 ++++++++++ .../test/unit/src/util/artifacts.ts | 2 +- 13 files changed, 440 insertions(+), 37 deletions(-) create mode 100644 garden-service/src/commands/login.ts create mode 100644 garden-service/src/commands/logout.ts create mode 100644 garden-service/src/db/entities/client-auth-token.ts create mode 100644 garden-service/src/platform/auth.ts create mode 100644 garden-service/test/unit/src/platform/auth.ts diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index 7fa765d89b..1250fb90a9 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -863,6 +863,14 @@ "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } } } }, @@ -4683,11 +4691,6 @@ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.0.0.tgz", "integrity": "sha512-ZuNWHD7S7IoikyEmx35vPU8H1W0L+oi644+4mSTg7nwXvBQpIwQL7DPjYUF0VMB0jPkNMo3MqD07E7MYrkFmjQ==" }, - "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" - }, "execa": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.0.tgz", @@ -7437,6 +7440,11 @@ "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", "dev": true }, + "is-docker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==" + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -7613,6 +7621,11 @@ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" }, + "is-wsl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", + "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -9736,6 +9749,15 @@ "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" }, + "open": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.0.2.tgz", + "integrity": "sha512-70E/pFTPr7nZ9nLDPNTcj3IVqnNvKuP4VsBmoKV9YGTnChe0mlS3C4qM7qKarhZ8rGaHKLfo+vBTHXDp6ZSyLQ==", + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, "openid-client": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-2.5.0.tgz", @@ -9967,25 +9989,6 @@ } } }, - "p-queue": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.3.0.tgz", - "integrity": "sha512-fg5dJlFpd5+3CgG3/0ogpVZUeJbjiyXFg0nu53hrOYsybqSiDyxyOpad0Rm6tAiGjgztAwkyvhlYHC53OiAJOA==", - "requires": { - "eventemitter3": "^4.0.0", - "p-timeout": "^3.1.0" - }, - "dependencies": { - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "requires": { - "p-finally": "^1.0.0" - } - } - } - }, "p-retry": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz", @@ -13195,12 +13198,9 @@ } }, "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "requires": { - "async-limiter": "~1.0.0" - } + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz", + "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==" }, "xml-js": { "version": "1.6.11", diff --git a/garden-service/package.json b/garden-service/package.json index 61c10d4b20..d8d1377929 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -98,7 +98,7 @@ "node-forge": "^0.9.1", "normalize-path": "^3.0.0", "normalize-url": "^5.0.0", - "p-queue": "^6.3.0", + "open": "^7.0.2", "p-retry": "^4.2.0", "parse-git-config": "^3.0.0", "path-is-inside": "^1.0.2", @@ -129,6 +129,7 @@ "uuid": "^7.0.2", "winston": "^3.2.1", "wrap-ansi": "^6.2.0", + "ws": "^7.2.1", "xml-js": "^1.6.11" }, "devDependencies": { diff --git a/garden-service/src/cli/cli.ts b/garden-service/src/cli/cli.ts index 0e27172886..f0a5cb8f97 100644 --- a/garden-service/src/cli/cli.ts +++ b/garden-service/src/cli/cli.ts @@ -13,7 +13,7 @@ import { resolve, join } from "path" import { safeDump } from "js-yaml" import { coreCommands } from "../commands/commands" import { DeepPrimitiveMap } from "../config/common" -import { shutdown, sleep, getPackageVersion } from "../util/util" +import { shutdown, sleep, getPackageVersion, uuidv4 } from "../util/util" import { deline } from "../util/string" import { BooleanParameter, @@ -302,6 +302,8 @@ export class GardenCli { logger.info("") const footerLog = logger.placeholder() + const sessionId = uuidv4() + const contextOpts: GardenOpts = { commandInfo: { name: command.getFullName(), @@ -310,6 +312,7 @@ export class GardenCli { }, environmentName, log, + sessionId, } let garden: Garden diff --git a/garden-service/src/commands/commands.ts b/garden-service/src/commands/commands.ts index 7708b90c9f..329901200f 100644 --- a/garden-service/src/commands/commands.ts +++ b/garden-service/src/commands/commands.ts @@ -30,6 +30,8 @@ import { ServeCommand } from "./serve" import { OptionsCommand } from "./options" import { ConfigCommand } from "./config/config" import { PluginsCommand } from "./plugins" +import { LoginCommand } from "./login" +import { LogOutCommand } from "./logout" export const coreCommands: Command[] = [ new BuildCommand(), @@ -55,4 +57,6 @@ export const coreCommands: Command[] = [ new UpdateRemoteCommand(), new ValidateCommand(), new ConfigCommand(), + new LoginCommand(), + new LogOutCommand(), ] diff --git a/garden-service/src/commands/login.ts b/garden-service/src/commands/login.ts new file mode 100644 index 0000000000..fa94cd5eac --- /dev/null +++ b/garden-service/src/commands/login.ts @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018-2020 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Command, CommandParams, CommandResult } from "./base" +import { printHeader } from "../logger/util" +import dedent = require("dedent") +import { login } from "../platform/auth" + +export class LoginCommand extends Command { + name = "login" + help = "Log in to Garden Cloud." + hidden = true + + description = dedent` + Logs you in to Garden Cloud. Subsequent commands will have access to platform features. + ` + + async action({ garden, log, headerLog }: CommandParams): Promise { + printHeader(headerLog, "Login", "cloud") + + if (garden.platformUrl) { + await login(garden.platformUrl, log) + } + + return {} + } +} diff --git a/garden-service/src/commands/logout.ts b/garden-service/src/commands/logout.ts new file mode 100644 index 0000000000..66ae7554d9 --- /dev/null +++ b/garden-service/src/commands/logout.ts @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018-2020 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Command, CommandParams, CommandResult } from "./base" +import { printHeader } from "../logger/util" +import dedent = require("dedent") +import { clearAuthToken } from "../platform/auth" + +export class LogOutCommand extends Command { + name = "logout" + help = "Log out of Garden Cloud." + hidden = true + + description = dedent` + Logs you out of Garden Cloud. + ` + + async action({ log, headerLog }: CommandParams): Promise { + printHeader(headerLog, "Log out", "cloud") + + await clearAuthToken(log) + + return {} + } +} diff --git a/garden-service/src/db/base-entity.ts b/garden-service/src/db/base-entity.ts index 6325ade2f5..a7b691d315 100644 --- a/garden-service/src/db/base-entity.ts +++ b/garden-service/src/db/base-entity.ts @@ -41,4 +41,12 @@ export class GardenEntity extends BaseEntity { const connection = getConnection() return connection.getRepository(this) } + + /** + * Helper method to avoid circular import issues. + */ + + static getConnection() { + return getConnection() + } } diff --git a/garden-service/src/db/connection.ts b/garden-service/src/db/connection.ts index c77f640592..65575dae37 100644 --- a/garden-service/src/db/connection.ts +++ b/garden-service/src/db/connection.ts @@ -9,20 +9,21 @@ import { Connection, getConnectionManager, ConnectionOptions } from "typeorm" import { join } from "path" import { GARDEN_GLOBAL_PATH } from "../constants" -import { LocalAddress } from "./entities/local-address" let connection: Connection // Note: This function needs to be synchronous to work with the typeorm Active Record pattern (see ./base-entity.ts) export function getConnection(): Connection { if (!connection) { + const { LocalAddress } = require("./entities/local-address") + const { ClientAuthToken } = require("./entities/client-auth-token") // Prepare the connection (the ormconfig.json in the static dir is only used for the typeorm CLI during dev) const options: ConnectionOptions = { type: "sqlite", database: join(GARDEN_GLOBAL_PATH, "db"), // IMPORTANT: All entities and migrations need to be manually referenced here because of how we // package the garden binary - entities: [LocalAddress], + entities: [LocalAddress, ClientAuthToken], migrations: [], // Auto-create new tables on init synchronize: true, diff --git a/garden-service/src/db/entities/client-auth-token.ts b/garden-service/src/db/entities/client-auth-token.ts new file mode 100644 index 0000000000..3a3e99ce73 --- /dev/null +++ b/garden-service/src/db/entities/client-auth-token.ts @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2018-2020 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Entity, Column, Index } from "typeorm" +import { GardenEntity } from "../base-entity" + +@Entity() +@Index(["token"], { unique: true }) +export class ClientAuthToken extends GardenEntity { + @Column() + token: string +} diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 416625420a..d763799979 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -21,7 +21,7 @@ import { Module, getModuleCacheContext, getModuleKey, ModuleConfigMap } from "./ import { pluginModuleSchema, ModuleTypeMap } from "./types/plugin/plugin" import { SourceConfig, ProjectConfig, resolveProjectConfig, pickEnvironment, OutputSpec } from "./config/project" import { findByName, pickKeys, getPackageVersion, getNames, findByNames } from "./util/util" -import { ConfigurationError, PluginError, RuntimeError } from "./exceptions" +import { ConfigurationError, PluginError, RuntimeError, InternalError } from "./exceptions" import { VcsHandler, ModuleVersion } from "./vcs/vcs" import { GitHandler } from "./vcs/git" import { BuildDir } from "./build-dir" @@ -61,6 +61,7 @@ import { deline, naturalList } from "./util/string" import { ensureConnected } from "./db/connection" import { DependencyValidationGraph } from "./util/validate-dependencies" import { Profile } from "./util/profiling" +import { readAuthToken, login } from "./platform/auth" export interface ActionHandlerMap { [actionName: string]: PluginActionHandlers[T] @@ -92,19 +93,22 @@ export interface GardenOpts { persistent?: boolean log?: LogEntry plugins?: RegisterPluginParam[] + sessionId?: string } export interface GardenParams { artifactsPath: string buildDir: BuildDir - environmentName: string + clientAuthToken: string | null dotIgnoreFiles: string[] + environmentName: string gardenDirPath: string log: LogEntry moduleIncludePatterns?: string[] moduleExcludePatterns?: string[] opts: GardenOpts outputs: OutputSpec[] + platformUrl: string | null plugins: RegisterPluginParam[] production: boolean projectName: string @@ -112,6 +116,7 @@ export interface GardenParams { projectSources?: SourceConfig[] providerConfigs: ProviderConfig[] variables: DeepPrimitiveMap + sessionId: string | null vcs: VcsHandler workingCopyId: string } @@ -129,6 +134,11 @@ export class Garden { private watcher: Watcher private asyncLock: any + // Platform-related instance variables + public clientAuthToken: string | null + public platformUrl: string | null + public sessionId: string | null + public readonly configStore: ConfigStore public readonly globalConfigStore: GlobalConfigStore public readonly vcs: VcsHandler @@ -158,6 +168,9 @@ export class Garden { constructor(params: GardenParams) { this.buildDir = params.buildDir + this.clientAuthToken = params.clientAuthToken + this.platformUrl = params.platformUrl + this.sessionId = params.sessionId this.environmentName = params.environmentName this.gardenDirPath = params.gardenDirPath this.log = params.log @@ -265,8 +278,30 @@ export class Garden { // Connect to the state storage await ensureConnected() + const sessionId = opts.sessionId || null + + // TODO: Read the platformUrl from config. + const platformUrl = process.env.GARDEN_CLOUD ? `http://${process.env.GARDEN_CLOUD}` : null + + const clientAuthToken = await readAuthToken(log) + // If a client auth token exists in local storage, we assume that the user wants to be logged in to the platform. + if (clientAuthToken && sessionId) { + if (!platformUrl) { + const errMsg = deline` + GARDEN_CLOUD environment variable is not set. Make sure it is set to the appropriate API + backend endpoint (e.g. myusername-cloud-api.cloud.dev.garden.io, without an http/https + prefix).` + throw new InternalError(errMsg, {}) + } else { + await login(platformUrl, log) + } + } + const garden = new this({ artifactsPath, + clientAuthToken, + sessionId, + platformUrl, projectRoot, projectName, environmentName, diff --git a/garden-service/src/platform/auth.ts b/garden-service/src/platform/auth.ts new file mode 100644 index 0000000000..b8d1fa61c8 --- /dev/null +++ b/garden-service/src/platform/auth.ts @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2018-2020 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import axios from "axios" +import qs = require("qs") +import open from "open" +import { Server } from "http" +import Koa from "koa" +import { EventEmitter2 } from "eventemitter2" +import bodyParser = require("koa-bodyparser") +import Router = require("koa-router") +import getPort = require("get-port") +import { ClientAuthToken } from "../db/entities/client-auth-token" +import { LogEntry } from "../logger/log-entry" + +// TODO: Add error handling and tests for all of this + +/** + * Logs in to the platform if needed, and returns a valid client auth token. + */ +export async function login(platformUrl: string, log: LogEntry): Promise { + const savedToken = await readAuthToken(log) + + // Ping platform with saved token (if it exists) + if (savedToken) { + log.debug("Local client auth token found, verifying it with platform...") + if (await checkClientAuthToken(savedToken, platformUrl, log)) { + log.debug("Local client token is valid, no need for login.") + return savedToken + } + } + + /** + * Else, start auth redirect server and wait for its redirect handler to receive + * the redirect and finish running. + */ + const events = new EventEmitter2() + const server = new AuthRedirectServer(platformUrl, events, log) + log.debug(`Redirecting to platform login page...`) + const newToken: string = await new Promise(async (resolve, _reject) => { + // The server resolves the promise with the new auth token once it's received the redirect. + await server.start() + events.once("receivedToken", ({ token }: { token: string }) => { + log.debug("Received client auth token.") + resolve(token) + }) + }) + await server.close() + await saveAuthToken(newToken, log) + return newToken +} + +/** + * Checks with the backend whether the provided client auth token is valid. + */ +async function checkClientAuthToken(token: string, platformUrl: string, log: LogEntry): Promise { + const res = await axios.get(`${platformUrl}/token/verify?${qs.stringify({ token })}`) + log.debug(`Checked client auth token with platform - valid: ${res.data.data.valid}`) + return !!res.data.data.valid +} + +/** + * We make a transaction deleting all existing client auth tokens and creating a new token. + * + * This also covers the inconsistent/erroneous case of more than one auth token existing in the local store. + */ +export async function saveAuthToken(token: string, log: LogEntry) { + try { + const manager = ClientAuthToken.getConnection().manager + await manager.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.clear(ClientAuthToken) + await transactionalEntityManager.save(ClientAuthToken, ClientAuthToken.create({ token })) + }) + log.debug("Saved client auth token to local config db") + } catch (error) { + log.error(`An error occurred while saving client auth token to local config db:\n${error.message}`) + } +} + +/** + * If a persisted client auth token was found, returns it. Returns null otherwise. + * + * In the inconsistent/erroneous case of more than one auth token existing in the local store, picks the first auth + * token and deletes all others. + */ +export async function readAuthToken(log: LogEntry): Promise { + const [tokens, tokenCount] = await ClientAuthToken.findAndCount() + + const token = tokens[0] ? tokens[0].token : null + + if (tokenCount > 1) { + log.debug("More than one client auth tokens found, clearing up...") + try { + await ClientAuthToken.getConnection() + .createQueryBuilder() + .delete() + .from(ClientAuthToken) + .where("token != :token", { token }) + .execute() + } catch (error) { + log.error(`An error occurred while clearing up duplicate client auth tokens:\n${error.message}`) + } + } + log.debug("Retrieved client auth token from local config db") + + return token +} + +/** + * If a persisted client auth token exists, deletes it. + */ +export async function clearAuthToken(log: LogEntry) { + await ClientAuthToken.getConnection() + .createQueryBuilder() + .delete() + .from(ClientAuthToken) + .execute() + log.debug("Cleared persisted auth token (if any)") +} + +// TODO: Add analytics tracking +export class AuthRedirectServer { + private log: LogEntry + private server: Server + private app: Koa + private platformUrl: string + private events: EventEmitter2 + + constructor(platformUrl: string, events: EventEmitter2, log: LogEntry, public port?: number) { + this.platformUrl = platformUrl + this.events = events + this.log = log.placeholder() + } + + async start() { + if (this.app) { + return + } + + if (!this.port) { + this.port = await getPort() + } + + await this.createApp() + + const query = { cliport: `${this.port}` } + await open(`${this.platformUrl}/cli/login/?${qs.stringify(query)}`) + } + + async close() { + this.log.debug("Shutting down redirect server...") + return this.server.close() + } + + async createApp() { + const app = new Koa() + const http = new Router() + http.get("/", async (ctx) => { + const token = ctx.request.query.jwt + this.log.debug("Received client auth token") + this.events.emit("receivedToken", { token }) + ctx.redirect("http://www.garden.io") + }) + app.use(bodyParser()) + app.use(http.allowedMethods()) + app.use(http.routes()) + app.on("error", (err) => { + this.log.error(`Auth redirect request failed with status ${err.status}: ${err.message}`) + }) + this.server = app.listen(this.port) + } +} diff --git a/garden-service/test/unit/src/platform/auth.ts b/garden-service/test/unit/src/platform/auth.ts new file mode 100644 index 0000000000..7d7ebad338 --- /dev/null +++ b/garden-service/test/unit/src/platform/auth.ts @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018-2020 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import Bluebird from "bluebird" +import { expect } from "chai" +import { ClientAuthToken } from "../../../../src/db/entities/client-auth-token" +import { makeTestGardenA } from "../../../helpers" +import { saveAuthToken, readAuthToken, clearAuthToken } from "../../../../src/platform/auth" + +async function cleanupAuthTokens() { + await ClientAuthToken.createQueryBuilder() + .delete() + .execute() +} + +/** + * Note: Running these tests locally will delete your saved auth token, if any. + */ +describe("auth", () => { + after(cleanupAuthTokens) + + describe("saveAuthToken", () => { + beforeEach(cleanupAuthTokens) + + it("should persist an auth token to the local config db", async () => { + const garden = await makeTestGardenA() + await saveAuthToken("test-token", garden.log) + const savedToken = await ClientAuthToken.findOne() + expect(savedToken).to.exist + expect(savedToken!.token).to.eql("test-token") + }) + + it("should never persist more than one auth token to the local config db", async () => { + const garden = await makeTestGardenA() + await Bluebird.map(["token-a", "token-b", "token-c"], async (token) => { + await saveAuthToken(token, garden.log) + }) + const count = await ClientAuthToken.count() + expect(count).to.eql(1) + }) + }) + + describe("readAuthToken", () => { + beforeEach(cleanupAuthTokens) + + it("should return null when no auth token is present", async () => { + const garden = await makeTestGardenA() + const savedToken = await readAuthToken(garden.log) + expect(savedToken).to.be.null + }) + + it("should return a saved auth token when one exists", async () => { + const garden = await makeTestGardenA() + const testToken = "test-token" + await saveAuthToken(testToken, garden.log) + const savedToken = await readAuthToken(garden.log) + expect(savedToken).to.eql("test-token") + }) + + it("should clean up duplicate auth tokens in the erroneous case when several exist", async () => { + const garden = await makeTestGardenA() + await Bluebird.map(["token-1", "token-2", "token-3"], async (token) => { + await ClientAuthToken.createQueryBuilder() + .insert() + .values({ token }) + .execute() + }) + await readAuthToken(garden.log) + const count = await ClientAuthToken.count() + expect(count).to.eql(1) + }) + }) + + describe("clearAuthToken", () => { + beforeEach(cleanupAuthTokens) + + it("should delete a saved auth token", async () => { + const garden = await makeTestGardenA() + await saveAuthToken("test-token", garden.log) + await clearAuthToken(garden.log) + const count = await ClientAuthToken.count() + expect(count).to.eql(0) + }) + + it("should not throw an exception if no auth token exists", async () => { + const garden = await makeTestGardenA() + await clearAuthToken(garden.log) + }) + }) +}) diff --git a/garden-service/test/unit/src/util/artifacts.ts b/garden-service/test/unit/src/util/artifacts.ts index 754f6efdd0..02c7c69dda 100644 --- a/garden-service/test/unit/src/util/artifacts.ts +++ b/garden-service/test/unit/src/util/artifacts.ts @@ -15,7 +15,7 @@ import { getArtifactFileList, getArtifactKey } from "../../../../src/util/artifa import { getLogger } from "../../../../src/logger/logger" describe("artifacts", () => { - describe("getArtifcatKey", () => { + describe("getArtifactKey", () => { it("should return the artifact key with format type.name.version", () => { expect(getArtifactKey("task", "task-name", "v-123456")).to.equal("task.task-name.v-123456") expect(getArtifactKey("test", "test-name", "v-123456")).to.equal("test.test-name.v-123456")