Skip to content

Commit

Permalink
test: avoid actual Cloud API calls in unit/functional tests
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald committed Jun 16, 2023
1 parent 71bd314 commit d5176dc
Show file tree
Hide file tree
Showing 15 changed files with 216 additions and 140 deletions.
6 changes: 3 additions & 3 deletions cli/test/unit/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { resolve } from "path"
import { runCli, getBundledPlugins } from "../../../src/cli"
import { testRoot } from "../../helpers"

import { GardenCli } from "@garden-io/core/build/src/cli/cli"
import { projectRootA } from "@garden-io/core/build/test/helpers"
import { TestGardenCli } from "@garden-io/core/build/test/helpers/cli"
import { Command, CommandParams } from "@garden-io/core/build/src/commands/base"
import { randomString } from "@garden-io/core/build/src/util/string"
import { GlobalConfigStore } from "@garden-io/core/build/src/config-store/global"
Expand Down Expand Up @@ -59,7 +59,7 @@ describe("runCli", () => {
}
}

const cli = new GardenCli()
const cli = new TestGardenCli()
const cmd = new TestCommand()
cli.addCommand(cmd)

Expand All @@ -84,7 +84,7 @@ describe("runCli", () => {
}
}

const cli = new GardenCli()
const cli = new TestGardenCli()
const cmd = new TestCommand()
cli.addCommand(cmd)

Expand Down
14 changes: 11 additions & 3 deletions core/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { ERROR_LOG_FILENAME, DEFAULT_GARDEN_DIR_NAME, LOGS_DIR_NAME, gardenEnv }
import { generateBasicDebugInfoReport } from "../commands/get/get-debug-info"
import { AnalyticsHandler } from "../analytics/analytics"
import { GardenPluginReference } from "../plugin/plugin"
import { CloudApi, CloudApiTokenRefreshError, getGardenCloudDomain } from "../cloud/api"
import { CloudApi, CloudApiFactory, CloudApiTokenRefreshError, getGardenCloudDomain } from "../cloud/api"
import { findProjectConfig } from "../config/base"
import { pMemoizeDecorator } from "../lib/p-memoize"
import { getCustomCommands } from "../commands/custom"
Expand All @@ -59,6 +59,12 @@ export interface RunOutput {
consoleOutput?: string
}

export interface GardenCliParams {
plugins?: GardenPluginReference[]
initLogger?: boolean
cloudApiFactory?: CloudApiFactory
}

// TODO: this is used in more contexts now, should rename to GardenCommandRunner or something like that
@Profile()
export class GardenCli {
Expand All @@ -67,10 +73,12 @@ export class GardenCli {
public plugins: GardenPluginReference[]
private initLogger: boolean
public processRecord: GardenProcess
protected cloudApiFactory: CloudApiFactory

constructor({ plugins, initLogger = false }: { plugins?: GardenPluginReference[]; initLogger?: boolean } = {}) {
constructor({ plugins, initLogger = false, cloudApiFactory = CloudApi.factory }: GardenCliParams = {}) {
this.plugins = plugins || []
this.initLogger = initLogger
this.cloudApiFactory = cloudApiFactory

const commands = sortBy(getBuiltinCommands(), (c) => c.name)
commands.forEach((command) => this.addCommand(command))
Expand Down Expand Up @@ -220,7 +228,7 @@ ${renderCommands(commands)}
const distroName = getCloudDistributionName(cloudDomain)

try {
cloudApi = await CloudApi.factory({ log, cloudDomain, globalConfigStore })
cloudApi = await this.cloudApiFactory({ log, cloudDomain, globalConfigStore })
} catch (err) {
if (err instanceof CloudApiTokenRefreshError) {
log.warn(dedent`
Expand Down
46 changes: 46 additions & 0 deletions core/src/cloud/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import {
GetProfileResponse,
CreateProjectsForRepoResponse,
ListProjectsResponse,
BaseResponse,
} from "@garden-io/platform-api-types"
import { getCloudDistributionName, getCloudLogSectionName, getPackageVersion } from "../util/util"
import { CommandInfo } from "../plugin-context"
import type { ClientAuthToken, GlobalConfigStore } from "../config-store/global"
import { add } from "date-fns"
import { LogLevel } from "../logger/logger"
import { makeAuthHeader } from "./auth"
import { StringMap } from "../config/common"

const gardenClientName = "garden-core"
const gardenClientVersion = getPackageVersion()
Expand Down Expand Up @@ -117,6 +119,12 @@ export interface CloudProject {
environments: CloudEnvironment[]
}

export interface GetSecretsParams {
log: Log
projectId: string
environmentName: string
}

function toCloudProject(
project: GetProjectResponse["data"] | ListProjectsResponse["data"][0] | CreateProjectsForRepoResponse["data"][0]
): CloudProject {
Expand Down Expand Up @@ -157,6 +165,8 @@ export interface CloudApiFactoryParams {
skipLogging?: boolean
}

export type CloudApiFactory = (params: CloudApiFactoryParams) => Promise<CloudApi | undefined>

/**
* The Enterprise API client.
*
Expand Down Expand Up @@ -715,4 +725,40 @@ export class CloudApi {
getRegisteredSession(sessionId: string) {
return this.registeredSessions.get(sessionId)
}

async getSecrets({ log, projectId, environmentName }: GetSecretsParams): Promise<StringMap> {
let secrets: StringMap = {}
const distroName = getCloudDistributionName(this.domain)

try {
const res = await this.get<BaseResponse>(`/secrets/projectUid/${projectId}/env/${environmentName}`)
secrets = res.data
} catch (err) {
if (isGotError(err, 404)) {
log.debug(`No secrets were received from ${distroName}.`)
log.debug("")
log.debug(deline`
Either the environment ${environmentName} does not exist in ${distroName}, or no project
with the id in your project configuration exists in ${distroName}.
`)
log.debug("")
log.debug(deline`
Please visit ${this.domain} to review the environments and projects currently
in the system.
`)
} else {
throw err
}
}

const emptyKeys = Object.keys(secrets).filter((key) => !secrets[key])
if (emptyKeys.length > 0) {
const prefix =
emptyKeys.length === 1
? "The following secret key has an empty value"
: "The following secret keys have empty values"
log.error(`${prefix}: ${emptyKeys.sort().join(", ")}`)
}
return secrets
}
}
57 changes: 0 additions & 57 deletions core/src/cloud/get-secrets.ts

This file was deleted.

7 changes: 4 additions & 3 deletions core/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ import {
import { CloudApi, CloudProject, CloudApiDuplicateProjectsError, getGardenCloudDomain } from "./cloud/api"
import { OutputConfigContext } from "./config/template-contexts/module"
import { ProviderConfigContext } from "./config/template-contexts/provider"
import { getSecrets } from "./cloud/get-secrets"
import type { ConfigContext } from "./config/template-contexts/base"
import { validateSchema, validateWithPath } from "./config/validation"
import { pMemoizeDecorator } from "./lib/p-memoize"
Expand Down Expand Up @@ -1784,6 +1783,7 @@ export const resolveGardenParams = profileAsync(async function _resolveGardenPar

try {
cloudProject = await cloudApi.getOrCreateProjectByName(projectName)
cloudLog.debug(`${distroName} project ID: ${cloudProject.id}`)
} catch (err) {
if (err instanceof CloudApiDuplicateProjectsError) {
cloudLog.warn(chalk.yellow(wordWrap(err.message, 120)))
Expand All @@ -1799,9 +1799,10 @@ export const resolveGardenParams = profileAsync(async function _resolveGardenPar

// Only fetch secrets if the projectId exists in the cloud API instance
try {
secrets = await getSecrets({ log: cloudLog, projectId: cloudProject.id, environmentName, cloudApi })
cloudLog.debug(`Fetching secrets from ${cloudDomain}.`)
secrets = await cloudApi.getSecrets({ log: cloudLog, projectId: cloudProject.id, environmentName })
cloudLog.verbose(chalk.green("Ready"))
cloudLog.debug(`Fetched ${Object.keys(secrets).length} secrets from ${cloudDomain}`)
cloudLog.debug(`Fetched ${Object.keys(secrets).length} secrets from ${cloudDomain}.`)
} catch (err) {
cloudLog.debug(`Fetching secrets failed with error: ${err.message}`)
}
Expand Down
8 changes: 6 additions & 2 deletions core/src/server/instance-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import chalk from "chalk"
import { Autocompleter, AutocompleteSuggestion } from "../cli/autocomplete"
import { parseCliVarFlags } from "../cli/helpers"
import { ParameterValues } from "../cli/params"
import { CloudApi, CloudApiFactoryParams, getGardenCloudDomain } from "../cloud/api"
import { CloudApi, CloudApiFactory, CloudApiFactoryParams, getGardenCloudDomain } from "../cloud/api"
import type { Command } from "../commands/base"
import { getBuiltinCommands, flattenCommands } from "../commands/commands"
import { getCustomCommands } from "../commands/custom"
Expand Down Expand Up @@ -47,6 +47,7 @@ interface GardenInstanceManagerParams {
serveCommand?: ServeCommand
extraCommands?: Command[]
defaultOpts?: Partial<GardenOpts>
cloudApiFactory?: CloudApiFactory
}

// TODO: clean up unused instances after some timeout since last request and when no monitors are active
Expand All @@ -59,6 +60,7 @@ export class GardenInstanceManager {
private plugins: GardenPluginReference[]
private instances: Map<string, InstanceContext>
private projectRoots: Map<string, ProjectRootContext>
private cloudApiFactory: CloudApiFactory
private cloudApis: Map<string, CloudApi>
private lastRequested: Map<string, Date>
private lock: AsyncLock
Expand All @@ -84,6 +86,7 @@ export class GardenInstanceManager {
extraCommands,
defaultOpts,
plugins,
cloudApiFactory,
}: GardenInstanceManagerParams) {
this.sessionId = sessionId
this.instances = new Map()
Expand All @@ -92,6 +95,7 @@ export class GardenInstanceManager {
this.lastRequested = new Map()
this.defaultOpts = defaultOpts || {}
this.plugins = plugins
this.cloudApiFactory = cloudApiFactory || CloudApi.factory

this.events = new EventBus()
this.monitors = new MonitorManager(log, this.events)
Expand Down Expand Up @@ -182,7 +186,7 @@ export class GardenInstanceManager {
let api = this.cloudApis.get(cloudDomain)

if (!api) {
api = await CloudApi.factory(params)
api = await this.cloudApiFactory(params)
api && this.cloudApis.set(cloudDomain, api)
}

Expand Down
8 changes: 1 addition & 7 deletions core/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
ProviderHandlers,
RegisterPluginParam,
} from "../src/plugin/plugin"
import { Garden, GardenOpts } from "../src/garden"
import { Garden } from "../src/garden"
import { ModuleConfig } from "../src/config/module"
import { ModuleVersion } from "../src/vcs/vcs"
import { DEFAULT_BUILD_TIMEOUT_SEC, GARDEN_CORE_ROOT, GardenApiVersion, gardenEnv } from "../src/constants"
Expand Down Expand Up @@ -438,12 +438,6 @@ export const defaultModuleConfig: ModuleConfig = {
taskConfigs: [],
}

export class TestGardenCli extends GardenCli {
async getGarden(workingDir: string, opts: GardenOpts) {
return makeTestGarden(workingDir, opts)
}
}

export const makeTestModule = (params: Partial<ModuleConfig> = {}): ModuleConfig => {
// deep merge `params` config into `defaultModuleConfig`
return merge(cloneDeep(defaultModuleConfig), params)
Expand Down
86 changes: 86 additions & 0 deletions core/test/helpers/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2018-2023 Garden Technologies, Inc. <[email protected]>
*
* 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 { CloudApi, CloudProject, GetSecretsParams } from "../../src/cloud/api"
import { Log } from "../../src/logger/log-entry"
import { GlobalConfigStore } from "../../src/config-store/global"
import { uuidv4 } from "../../src/util/random"
import { StringMap } from "../../src/config/common"

export const apiProjectId = uuidv4()
export const apiRemoteOriginUrl = "[email protected]:garden-io/garden.git"
// The sha512 hash of "test-project-a"
export const apiProjectName =
"95048f63dc14db38ed4138ffb6ff89992abdc19b8c899099c52a94f8fcc0390eec6480385cfa5014f84c0a14d4984825ce3bf25db1386d2b5382b936899df675"

export class FakeCloudApi extends CloudApi {
static async factory(params: { log: Log; skipLogging?: boolean }) {
return new FakeCloudApi({
log: params.log,
domain: "https://garden.io",
globalConfigStore: new GlobalConfigStore(),
})
}

async getProfile() {
return {
id: "1",
createdAt: new Date().toString(),
updatedAt: new Date().toString(),
name: "gordon",
vcsUsername: "[email protected]",
serviceAccount: false,
organization: {
id: "1",
name: "garden",
},
cachedPermissions: {},
accessTokens: [],
groups: [],
}
}

async getAllProjects(): Promise<CloudProject[]> {
return [(await this.getProjectById(apiProjectId))!]
}

async createProject(name: string): Promise<CloudProject> {
return {
id: apiProjectId,
name,
repositoryUrl: apiRemoteOriginUrl,
environments: [],
}
}

async getProjectByName(name: string): Promise<CloudProject | undefined> {
return {
id: apiProjectId,
name,
repositoryUrl: apiRemoteOriginUrl,
environments: [],
}
}

async getProjectById(_: string): Promise<CloudProject | undefined> {
return {
id: apiProjectId,
name: apiProjectName,
repositoryUrl: apiRemoteOriginUrl,
environments: [],
}
}

async getSecrets(_: GetSecretsParams): Promise<StringMap> {
return {}
}

async checkClientAuthToken(): Promise<boolean> {
return true
}
}
Loading

0 comments on commit d5176dc

Please sign in to comment.