From 408a94c596e348d369329f7195b7f88e3bcbd73e Mon Sep 17 00:00:00 2001 From: Matt Seddon Date: Thu, 27 Apr 2023 15:04:31 +1000 Subject: [PATCH] use dvc config to store and access studio.token --- extension/src/cli/dvc/constants.ts | 16 +- extension/src/cli/dvc/executor.ts | 5 + extension/src/cli/dvc/index.ts | 8 +- extension/src/cli/dvc/reader.ts | 2 +- extension/src/extension.ts | 1 - extension/src/setup/index.ts | 188 ++++++++++++------ extension/src/telemetry/uuid.ts | 16 +- .../src/test/suite/experiments/index.test.ts | 2 +- extension/src/test/suite/setup/index.test.ts | 99 +++++---- extension/src/test/suite/setup/util.ts | 25 +-- extension/src/util/appdirs.test.ts | 59 ++++++ extension/src/util/appdirs.ts | 15 ++ 12 files changed, 286 insertions(+), 150 deletions(-) create mode 100644 extension/src/util/appdirs.test.ts create mode 100644 extension/src/util/appdirs.ts diff --git a/extension/src/cli/dvc/constants.ts b/extension/src/cli/dvc/constants.ts index f93fb3efcd..023aece588 100644 --- a/extension/src/cli/dvc/constants.ts +++ b/extension/src/cli/dvc/constants.ts @@ -20,6 +20,7 @@ export enum Command { ADD = 'add', CHECKOUT = 'checkout', COMMIT = 'commit', + CONFIG = 'config', DATA = 'data', EXPERIMENT = 'exp', INITIALIZE = 'init', @@ -46,7 +47,9 @@ export enum Flag { ALL_COMMITS = '-A', FOLLOW = '-f', FORCE = '-f', + GLOBAL = '--global', GRANULAR = '--granular', + LOCAL = '--local', JOBS = '-j', JSON = '--json', KILL = '--kill', @@ -56,6 +59,7 @@ export enum Flag { SET_PARAM = '-S', SPLIT = '--split', UNCHANGED = '--unchanged', + UNSET = '--unset', VERSION = '--version' } @@ -90,8 +94,18 @@ export enum GcPreserveFlag { WORKSPACE = '--workspace' } +export enum ConfigKey { + STUDIO_TOKEN = 'studio.token' +} + type Target = string type Flags = Flag | ExperimentFlag | GcPreserveFlag -export type Args = (Command | Target | ExperimentSubCommand | Flags)[] +export type Args = ( + | Command + | Target + | ExperimentSubCommand + | Flags + | ConfigKey +)[] diff --git a/extension/src/cli/dvc/executor.ts b/extension/src/cli/dvc/executor.ts index 1fffe846ca..d551dff589 100644 --- a/extension/src/cli/dvc/executor.ts +++ b/extension/src/cli/dvc/executor.ts @@ -18,6 +18,7 @@ export const autoRegisteredCommands = { ADD: 'add', CHECKOUT: 'checkout', COMMIT: 'commit', + CONFIG: 'config', EXPERIMENT_APPLY: 'experimentApply', EXPERIMENT_BRANCH: 'experimentBranch', EXPERIMENT_GARBAGE_COLLECT: 'experimentGarbageCollect', @@ -71,6 +72,10 @@ export class DvcExecutor extends DvcCli { return this.blockAndExecuteProcess(cwd, Command.COMMIT, ...args, Flag.FORCE) } + public config(cwd: string, ...args: Args) { + return this.executeDvcProcess(cwd, Command.CONFIG, ...args) + } + public experimentApply(cwd: string, experimentName: string) { return this.executeExperimentProcess( cwd, diff --git a/extension/src/cli/dvc/index.ts b/extension/src/cli/dvc/index.ts index ddab296694..4a9777a97d 100644 --- a/extension/src/cli/dvc/index.ts +++ b/extension/src/cli/dvc/index.ts @@ -7,7 +7,7 @@ import { Config } from '../../config' export class DvcCli extends Cli { public autoRegisteredCommands: string[] = [] - protected readonly config: Config + protected readonly extensionConfig: Config constructor( config: Config, @@ -18,7 +18,7 @@ export class DvcCli extends Cli { ) { super(emitters) - this.config = config + this.extensionConfig = config } public executeDvcProcess(cwd: string, ...args: Args): Promise { @@ -36,8 +36,8 @@ export class DvcCli extends Cli { protected getOptions(cwd: string, ...args: Args) { return getOptions( - this.config.getPythonBinPath(), - this.config.getCliPath(), + this.extensionConfig.getPythonBinPath(), + this.extensionConfig.getCliPath(), cwd, ...args ) diff --git a/extension/src/cli/dvc/reader.ts b/extension/src/cli/dvc/reader.ts index e9b0238212..8943e39f42 100644 --- a/extension/src/cli/dvc/reader.ts +++ b/extension/src/cli/dvc/reader.ts @@ -121,7 +121,7 @@ export class DvcReader extends DvcCli { public globalVersion(cwd: string): Promise { const options = getOptions( undefined, - this.config.getCliPath(), + this.extensionConfig.getCliPath(), cwd, Flag.VERSION ) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 16081d01b3..9fd8799e12 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -182,7 +182,6 @@ export class Extension extends Disposable { this.setup = this.dispose.track( new Setup( - context, config, this.internalCommands, this.experiments, diff --git a/extension/src/setup/index.ts b/extension/src/setup/index.ts index 7ff15ea986..0cc1d73d56 100644 --- a/extension/src/setup/index.ts +++ b/extension/src/setup/index.ts @@ -1,11 +1,5 @@ -import { - Event, - EventEmitter, - ExtensionContext, - SecretStorage, - ViewColumn, - workspace -} from 'vscode' +import { join } from 'path' +import { Event, EventEmitter, ViewColumn, workspace } from 'vscode' import { Disposable, Disposer } from '@hediet/std/disposable' import isEmpty from 'lodash.isempty' import { SetupSection, SetupData as TSetupData } from './webview/contract' @@ -14,7 +8,7 @@ import { WebviewMessages } from './webview/messages' import { validateTokenInput } from './inputBox' import { findPythonBinForInstall } from './autoInstall' import { run, runWithRecheck, runWorkspace } from './runner' -import { STUDIO_ACCESS_TOKEN_KEY, isStudioAccessToken } from './token' +import { isStudioAccessToken } from './token' import { pickFocusedProjects } from './quickPick' import { BaseWebview } from '../webview' import { ViewKey } from '../webview/constants' @@ -47,11 +41,15 @@ import { createFileSystemWatcher } from '../fileSystem/watcher' import { EventName } from '../telemetry/constants' import { WorkspaceScale } from '../telemetry/collect' import { gitPath } from '../cli/git/constants' -import { DOT_DVC } from '../cli/dvc/constants' +import { Flag, ConfigKey as DvcConfigKey, DOT_DVC } from '../cli/dvc/constants' import { GLOBAL_WEBVIEW_DVCROOT } from '../webview/factory' -import { ConfigKey, getConfigValue } from '../vscode/config' +import { + ConfigKey as ExtensionConfigKey, + getConfigValue +} from '../vscode/config' import { getValidInput } from '../vscode/inputBox' import { Title } from '../vscode/title' +import { getDVCAppDir } from '../util/appdirs' export type SetupWebviewWebview = BaseWebview @@ -87,14 +85,12 @@ export class Setup private dotFolderWatcher?: Disposer - private readonly secrets: SecretStorage private studioAccessToken: string | undefined = undefined private studioIsConnected = false private focusedSection: SetupSection | undefined = undefined constructor( - context: ExtensionContext, config: Config, internalCommands: InternalCommands, experiments: WorkspaceExperiments, @@ -147,31 +143,15 @@ export class Setup this.watchConfigurationDetailsForChanges() this.watchDotFolderForChanges() this.watchPathForChanges(stopWatch) - - this.secrets = context.secrets - - void this.getSecret(STUDIO_ACCESS_TOKEN_KEY).then( - async studioAccessToken => { - this.studioAccessToken = studioAccessToken - await this.updateIsStudioConnected() - this.deferred.resolve() - } - ) - - this.dispose.track( - context.secrets.onDidChange(async e => { - if (e.key !== STUDIO_ACCESS_TOKEN_KEY) { - return - } - - this.studioAccessToken = await this.getSecret(STUDIO_ACCESS_TOKEN_KEY) - return this.updateIsStudioConnected() - }) - ) + this.watchDvcConfigs() this.dispose.track( workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ConfigKey.STUDIO_SHARE_EXPERIMENTS_LIVE)) { + if ( + e.affectsConfiguration( + ExtensionConfigKey.STUDIO_SHARE_EXPERIMENTS_LIVE + ) + ) { return this.sendDataToWebview() } }) @@ -243,6 +223,7 @@ export class Setup public setCliCompatible(compatible: boolean | undefined) { this.cliCompatible = compatible + void this.updateIsStudioConnected() const incompatible = compatible === undefined ? undefined : !compatible void setContextValue(ContextKey.CLI_INCOMPATIBLE, incompatible) } @@ -256,7 +237,7 @@ export class Setup } public shouldBeShown() { - return !this.cliCompatible || !this.hasRoots() || !this.getHasData() + return !this.getCliCompatible() || !this.hasRoots() || !this.getHasData() } public async selectFocusedProjects() { @@ -300,11 +281,54 @@ export class Setup } } - public removeStudioAccessToken() { - return this.removeSecret(STUDIO_ACCESS_TOKEN_KEY) + public async removeStudioAccessToken() { + if (!this.getCliCompatible()) { + return + } + + if (this.dvcRoots.length !== 1) { + const cwd = getFirstWorkspaceFolder() + if (!cwd) { + return + } + return this.internalCommands.executeCommand( + AvailableCommands.CONFIG, + cwd, + Flag.GLOBAL, + Flag.UNSET, + DvcConfigKey.STUDIO_TOKEN + ) + } + + const cwd = this.dvcRoots[0] + + try { + await this.internalCommands.executeCommand( + AvailableCommands.CONFIG, + cwd, + Flag.LOCAL, + Flag.UNSET, + DvcConfigKey.STUDIO_TOKEN + ) + } catch {} + try { + return await this.internalCommands.executeCommand( + AvailableCommands.CONFIG, + cwd, + Flag.GLOBAL, + Flag.UNSET, + DvcConfigKey.STUDIO_TOKEN + ) + } catch {} } public async saveStudioAccessToken() { + const cwd = this.dvcRoots[0] || getFirstWorkspaceFolder() + + if (!cwd) { + return + } + const token = await getValidInput( Title.ENTER_STUDIO_TOKEN, validateTokenInput, @@ -314,12 +338,19 @@ export class Setup return } - return this.storeSecret(STUDIO_ACCESS_TOKEN_KEY, token) + await this.internalCommands.executeCommand( + AvailableCommands.CONFIG, + cwd, + Flag.GLOBAL, + DvcConfigKey.STUDIO_TOKEN, + token + ) + return this.updateIsStudioConnected() } public getStudioLiveShareToken() { return getConfigValue( - ConfigKey.STUDIO_SHARE_EXPERIMENTS_LIVE, + ExtensionConfigKey.STUDIO_SHARE_EXPERIMENTS_LIVE, false ) ? this.getStudioAccessToken() @@ -350,7 +381,7 @@ export class Setup this.webviewMessages.sendWebviewMessage({ canGitInitialize, - cliCompatible: this.cliCompatible, + cliCompatible: this.getCliCompatible(), hasData, isPythonExtensionInstalled: isPythonExtensionInstalled(), isStudioConnected: this.studioIsConnected, @@ -359,7 +390,9 @@ export class Setup projectInitialized, pythonBinPath: getBinDisplayText(pythonBinPath), sectionCollapsed: collectSectionCollapsed(this.focusedSection), - shareLiveToStudio: getConfigValue(ConfigKey.STUDIO_SHARE_EXPERIMENTS_LIVE) + shareLiveToStudio: getConfigValue( + ExtensionConfigKey.STUDIO_SHARE_EXPERIMENTS_LIVE + ) }) this.focusedSection = undefined } @@ -538,7 +571,11 @@ export class Setup const trySetupWithVenv = previousPythonBinPath !== this.config.getPythonBinPath() - if (!this.cliAccessible || !this.cliCompatible || trySetupWithVenv) { + if ( + !this.cliAccessible || + !this.getCliCompatible() || + trySetupWithVenv + ) { this.workspaceChanged.fire() } } @@ -579,6 +616,10 @@ export class Setup return this.workspaceChanged.fire() } + private getCliCompatible() { + return this.cliCompatible + } + private async getEventProperties() { return { ...(this.cliAccessible ? await this.collectWorkspaceScale() : {}), @@ -592,7 +633,8 @@ export class Setup } } - private updateIsStudioConnected() { + private async updateIsStudioConnected() { + await this.setStudioAccessToken() const storedToken = this.getStudioAccessToken() const isConnected = isStudioAccessToken(storedToken) return this.setStudioIsConnected(isConnected) @@ -604,22 +646,54 @@ export class Setup return setContextValue(ContextKey.STUDIO_CONNECTED, isConnected) } - private getSecret(key: string) { - const secrets = this.getSecrets() - return secrets.get(key) - } + private watchDvcConfigs() { + const createWatcher = (watchedPath: string) => + createFileSystemWatcher( + disposable => this.dispose.track(disposable), + getRelativePattern(watchedPath, '**'), + path => { + if ( + path.endsWith(join('dvc', 'config')) || + path.endsWith(join('dvc', 'config.local')) + ) { + void this.updateIsStudioConnected() + } + } + ) - private storeSecret(key: string, value: string) { - const secrets = this.getSecrets() - return secrets.store(key, value) - } + const globalConfigPath = getDVCAppDir() + + createWatcher(globalConfigPath) - private removeSecret(key: string) { - const secrets = this.getSecrets() - return secrets.delete(key) + for (const workspaceFolder of getWorkspaceFolders()) { + createWatcher(workspaceFolder) + } } - private getSecrets() { - return this.secrets + private async setStudioAccessToken() { + if (!this.getCliCompatible()) { + this.studioAccessToken = undefined + return + } + + if (this.dvcRoots.length !== 1) { + const cwd = getFirstWorkspaceFolder() + if (!cwd) { + this.studioAccessToken = undefined + return + } + this.studioAccessToken = await this.internalCommands.executeCommand( + AvailableCommands.CONFIG, + cwd, + DvcConfigKey.STUDIO_TOKEN + ) + return + } + + this.studioAccessToken = await this.internalCommands.executeCommand( + AvailableCommands.CONFIG, + this.dvcRoots[0], + DvcConfigKey.STUDIO_TOKEN + ) } } diff --git a/extension/src/telemetry/uuid.ts b/extension/src/telemetry/uuid.ts index 7568652d4d..afe713ab66 100644 --- a/extension/src/telemetry/uuid.ts +++ b/extension/src/telemetry/uuid.ts @@ -1,8 +1,8 @@ import { join } from 'path' import isEmpty from 'lodash.isempty' import { v4 } from 'uuid' -import { getProcessPlatform } from '../env' import { exists, loadJson, writeJson } from '../fileSystem' +import { getDVCAppDir, getIterativeAppDir } from '../util/appdirs' type UserConfig = { user_id?: string @@ -40,19 +40,15 @@ const writeMissingConfigs = ( } const readOrCreateConfig = (): string | undefined => { - const { userConfigDir } = require('appdirs') as { - userConfigDir: (appName: string) => string - } + const dvcAppDir = getDVCAppDir() + const iterativeAppDir = getIterativeAppDir() - const legacyDirectory = - getProcessPlatform() === 'win32' - ? join('iterative', 'dvc', 'user_id') - : join('dvc', 'user_id') + const legacyDirectory = join(dvcAppDir, 'user_id') - const legacyConfigPath = userConfigDir(legacyDirectory) + const legacyConfigPath = legacyDirectory const legacyConfig = loadConfig(legacyConfigPath) - const configPath = userConfigDir(join('iterative', 'telemetry')) + const configPath = join(iterativeAppDir, 'telemetry') const config = loadConfig(configPath) const user_id = legacyConfig.user_id || config.user_id || v4() diff --git a/extension/src/test/suite/experiments/index.test.ts b/extension/src/test/suite/experiments/index.test.ts index 7d6b973bd1..0cab2cc389 100644 --- a/extension/src/test/suite/experiments/index.test.ts +++ b/extension/src/test/suite/experiments/index.test.ts @@ -653,7 +653,7 @@ suite('Experiments Test Suite', () => { const tokenAccessed = new Promise(resolve => mockGetStudioAccessToken.callsFake(() => { resolve(undefined) - return undefined + return '' }) ) diff --git a/extension/src/test/suite/setup/index.test.ts b/extension/src/test/suite/setup/index.test.ts index ca9a3a215c..72bff3a0cd 100644 --- a/extension/src/test/suite/setup/index.test.ts +++ b/extension/src/test/suite/setup/index.test.ts @@ -4,7 +4,6 @@ import { ensureFileSync, remove } from 'fs-extra' import { expect } from 'chai' import { SinonStub, restore, spy, stub } from 'sinon' import { - EventEmitter, QuickPickItem, Uri, WorkspaceConfiguration, @@ -31,7 +30,7 @@ import { import { isDirectory } from '../../../fileSystem' import { gitPath } from '../../../cli/git/constants' import { join } from '../../util/path' -import { DOT_DVC } from '../../../cli/dvc/constants' +import { ConfigKey, DOT_DVC, Flag } from '../../../cli/dvc/constants' import * as Config from '../../../vscode/config' import { dvcDemoPath } from '../../util' import { @@ -43,10 +42,11 @@ import { StopWatch } from '../../../util/time' import { MIN_CLI_VERSION } from '../../../cli/dvc/contract' import { run } from '../../../setup/runner' import * as Python from '../../../extensions/python' -import { STUDIO_ACCESS_TOKEN_KEY } from '../../../setup/token' import { ContextKey } from '../../../vscode/context' import { Setup } from '../../../setup' import { SetupSection } from '../../../setup/webview/contract' +import { DvcExecutor } from '../../../cli/dvc/executor' +import { getFirstWorkspaceFolder } from '../../../vscode/workspaceFolders' suite('Setup Test Suite', () => { const disposable = Disposable.fn() @@ -681,70 +681,54 @@ suite('Setup Test Suite', () => { }).timeout(WEBVIEW_TEST_TIMEOUT) it("should handle a message from the webview to save the user's Studio access token", async () => { - const secretsChanged = disposable.track( - new EventEmitter<{ key: string }>() - ) const mockToken = 'isat_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - const mockSecrets: { [key: string]: string } = {} - - const onDidChange = secretsChanged.event - const mockGetSecret = (key: string): Promise => - Promise.resolve(mockSecrets[key]) - const mockStoreSecret = (key: string, value: string) => { - mockSecrets[key] = value - secretsChanged.fire({ key }) - return Promise.resolve(undefined) - } - - const mockSecretStorage = { - delete: stub(), - get: mockGetSecret, - onDidChange, - store: mockStoreSecret - } - - const secretsChangedEvent = new Promise(resolve => - onDidChange(() => resolve(undefined)) - ) - - const { setup, mockExecuteCommand } = buildSetup( - disposable, - false, - false, - false, - false, - mockSecretStorage - ) + const { setup, mockExecuteCommand, messageSpy } = buildSetup(disposable) mockExecuteCommand.restore() + const mockConfig = stub(DvcExecutor.prototype, 'config') + mockConfig.resolves('') + const executeCommandSpy = spy(commands, 'executeCommand') const webview = await setup.showWebview() await webview.isReady() const mockMessageReceived = getMessageReceivedEmitter(webview) - expect(executeCommandSpy).to.be.calledWithExactly( - 'setContext', - ContextKey.STUDIO_CONNECTED, - false - ) + mockConfig.resetBehavior() + mockConfig.resolves(mockToken) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + stub(Setup.prototype as any, 'getCliCompatible').returns(true) const mockInputBox = stub(window, 'showInputBox').resolves(mockToken) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - stub(Setup.prototype as any, 'getSecrets').returns(mockSecretStorage) + messageSpy.restore() + executeCommandSpy.resetHistory() + const messageSent = new Promise(resolve => + stub(BaseWebview.prototype, 'show').callsFake(() => { + resolve(undefined) + return Promise.resolve(true) + }) + ) mockMessageReceived.fire({ type: MessageFromWebviewType.SAVE_STUDIO_TOKEN }) - await secretsChangedEvent + await messageSent expect(mockInputBox).to.be.called - - expect(await mockGetSecret(STUDIO_ACCESS_TOKEN_KEY)).to.equal(mockToken) - + expect(mockConfig).to.be.calledWithExactly( + getFirstWorkspaceFolder(), + Flag.GLOBAL, + ConfigKey.STUDIO_TOKEN, + mockToken + ) + expect(mockConfig).to.be.calledWithExactly( + getFirstWorkspaceFolder(), + ConfigKey.STUDIO_TOKEN + ) expect(executeCommandSpy).to.be.calledWithExactly( 'setContext', ContextKey.STUDIO_CONNECTED, @@ -776,19 +760,26 @@ suite('Setup Test Suite', () => { ) }) - it('should be able to delete the Studio access token from secrets storage', async () => { - const mockDelete = stub() - stub( + it('should be able to delete the Studio access token from the global dvc config', async () => { + const mockConfig = stub( // eslint-disable-next-line @typescript-eslint/no-explicit-any - Setup.prototype as any, - 'getSecrets' - ).returns({ delete: mockDelete }) + DvcExecutor.prototype, + 'config' + ).resolves(undefined) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + stub(Setup.prototype as any, 'getCliCompatible').returns(true) await commands.executeCommand( RegisteredCommands.REMOVE_STUDIO_ACCESS_TOKEN ) - expect(mockDelete).to.be.calledWithExactly(STUDIO_ACCESS_TOKEN_KEY) + expect(mockConfig).to.be.calledWithExactly( + dvcDemoPath, + Flag.GLOBAL, + Flag.UNSET, + ConfigKey.STUDIO_TOKEN + ) }).timeout(WEBVIEW_TEST_TIMEOUT) it('should handle a message to open the experiments webview', async () => { diff --git a/extension/src/test/suite/setup/util.ts b/extension/src/test/suite/setup/util.ts index ae95867b86..d72d277b4c 100644 --- a/extension/src/test/suite/setup/util.ts +++ b/extension/src/test/suite/setup/util.ts @@ -1,11 +1,5 @@ import { join } from 'path' -import { - EventEmitter, - ExtensionContext, - SecretStorage, - commands, - env -} from 'vscode' +import { EventEmitter, commands, env } from 'vscode' import { Disposer } from '@hediet/std/disposable' import { fake, stub } from 'sinon' import { ensureDirSync } from 'fs-extra' @@ -32,14 +26,14 @@ export const buildSetup = ( hasData = false, noDvcRoot = true, noGitRoot = true, - noGitCommits = true, - mockSecretStorage: SecretStorage | undefined = undefined + noGitCommits = true ) => { const { config, messageSpy, resourceLocator, internalCommands, + dvcExecutor, dvcReader, gitExecutor, gitReader @@ -87,12 +81,6 @@ export const buildSetup = ( const setup = disposer.track( new Setup( - { - secrets: mockSecretStorage || { - get: stub().resolves(undefined), - onDidChange: stub() - } - } as unknown as ExtensionContext, config, internalCommands, { @@ -111,6 +99,7 @@ export const buildSetup = ( return { config, + dvcExecutor, internalCommands, messageSpy, mockAutoInstallDvc, @@ -146,12 +135,6 @@ export const buildSetupWithWatchers = async (disposer: Disposer) => { const setup = disposer.track( new Setup( - { - secrets: { - get: stub().resolves(undefined), - onDidChange: stub() - } - } as unknown as ExtensionContext, config, mockInternalCommands, { diff --git a/extension/src/util/appdirs.test.ts b/extension/src/util/appdirs.test.ts new file mode 100644 index 0000000000..d47f80f1bb --- /dev/null +++ b/extension/src/util/appdirs.test.ts @@ -0,0 +1,59 @@ +import { join } from 'path' +import { getDVCAppDir, getIterativeAppDir } from './appdirs' +import { getProcessPlatform } from '../env' + +const mockedUserConfigDir = require('appdirs').userConfigDir +const mockedGetProcessPlatform = jest.mocked(getProcessPlatform) +const mockedJoin = jest.mocked(join) + +jest.mock('appdirs', () => ({ userConfigDir: jest.fn() })) +jest.mock('../env') +jest.mock('path') + +beforeEach(() => { + jest.resetAllMocks() +}) + +describe('getIterativeAppDir', () => { + it('should return the correct path for non-windows platforms', () => { + mockedJoin.mockImplementation((...paths: string[]) => paths.join('/')) + const mockedAppDir = '/app/dir' + mockedUserConfigDir.mockImplementationOnce((path: string) => + join(mockedAppDir, path) + ) + expect(getIterativeAppDir()).toStrictEqual(mockedAppDir + '/' + 'iterative') + }) + it('should return the correct path on Windows', () => { + mockedJoin.mockImplementation((...paths: string[]) => paths.join('\\')) + mockedGetProcessPlatform.mockReturnValueOnce('win32') + const mockedAppDir = 'C:\\app\\dir' + mockedUserConfigDir.mockImplementationOnce((path: string) => + join(mockedAppDir, path) + ) + expect(getIterativeAppDir()).toStrictEqual( + join(mockedAppDir + '\\' + 'iterative') + ) + }) +}) + +describe('getDVCAppDir', () => { + it('should return the correct path for non-windows platforms', () => { + mockedJoin.mockImplementation((...paths: string[]) => paths.join('/')) + const mockedAppDir = '/app/dir' + mockedUserConfigDir.mockImplementationOnce((path: string) => + join(mockedAppDir, path) + ) + expect(getDVCAppDir()).toStrictEqual(mockedAppDir + '/' + 'dvc') + }) + it('should return the correct path on Windows', () => { + mockedJoin.mockImplementationOnce((...paths: string[]) => paths.join('\\')) + mockedGetProcessPlatform.mockReturnValueOnce('win32') + const mockedAppDir = 'C:\\app\\dir' + mockedUserConfigDir.mockImplementationOnce((path: string) => + join(mockedAppDir, path) + ) + expect(getDVCAppDir()).toStrictEqual( + join(mockedAppDir + '\\' + 'iterative' + 'dvc') + ) + }) +}) diff --git a/extension/src/util/appdirs.ts b/extension/src/util/appdirs.ts new file mode 100644 index 0000000000..0d14a599e6 --- /dev/null +++ b/extension/src/util/appdirs.ts @@ -0,0 +1,15 @@ +import { join } from 'path' +import { getProcessPlatform } from '../env' + +const { userConfigDir } = require('appdirs') as { + userConfigDir: (appName: string) => string +} + +export const getIterativeAppDir = (): string => userConfigDir('iterative') + +export const getDVCAppDir = (): string => { + if (getProcessPlatform() === 'win32') { + return join(getIterativeAppDir(), 'dvc') + } + return userConfigDir('dvc') +}