Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ExtContext } from '../../../shared/extensions'
import { addFolderToWorkspace } from '../../../shared/utilities/workspaceUtils'
import { ToolkitError } from '../../../shared/errors'
import { fs } from '../../../shared/fs/fs'
import { handleOverwriteConflict } from '../../../shared/utilities/messages'
import { getPattern } from '../../../shared/utilities/downloadPatterns'
import { MetadataManager } from './metadataManager'

Expand Down Expand Up @@ -89,6 +90,9 @@ export async function launchProjectCreationWizard(
export async function downloadPatternCode(config: CreateServerlessLandWizardForm, assetName: string): Promise<void> {
const fullAssetName = assetName + '.zip'
const location = vscode.Uri.joinPath(config.location, config.name)

await handleOverwriteConflict(location)

try {
await getPattern(serverlessLandOwner, serverlessLandRepo, fullAssetName, location, true)
} catch (error) {
Expand Down
38 changes: 38 additions & 0 deletions packages/core/src/shared/utilities/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as vscode from 'vscode'
import * as nls from 'vscode-nls'
import * as path from 'path'
import * as localizedText from '../localizedText'
import { getLogger } from '../../shared/logger/logger'
import { ProgressEntry } from '../../shared/vscode/window'
Expand All @@ -14,6 +15,8 @@ import { Timeout } from './timeoutUtils'
import { addCodiconToString } from './textUtilities'
import { getIcon, codicon } from '../icons'
import globals from '../extensionGlobals'
import { ToolkitError } from '../../shared/errors'
import { fs } from '../../shared/fs/fs'
import { openUrl } from './vsCodeUtils'
import { AmazonQPromptSettings, ToolkitPromptSettings } from '../../shared/settings'
import { telemetry, ToolkitShowNotification } from '../telemetry/telemetry'
Expand Down Expand Up @@ -140,6 +143,41 @@ export async function showViewLogsMessage(
})
}

/**
* Checks if a path exists and prompts user for overwrite confirmation if it does.
* @param path The file or directory path to check
* @param itemName The name of the item for display in the message
* @returns Promise<boolean> - true if should proceed (path doesn't exist or user confirmed overwrite)
*/
export async function handleOverwriteConflict(location: vscode.Uri): Promise<boolean> {
if (!(await fs.exists(location))) {
return true
}

const choice = showConfirmationMessage({
prompt: localize(
'AWS.toolkit.confirmOverwrite',
'{0} already exists in the selected directory, overwrite?',
location.fsPath
),
confirm: localize('AWS.generic.overwrite', 'Yes'),
cancel: localize('AWS.generic.cancel', 'No'),
type: 'warning',
})

if (!choice) {
throw new ToolkitError(`Folder already exists: ${path.basename(location.fsPath)}`)
}

try {
await fs.delete(location, { recursive: true, force: true })
} catch (error) {
throw ToolkitError.chain(error, `Failed to delete existing folder: ${path.basename(location.fsPath)}`)
}

return true
}

/**
* Shows a modal confirmation (warning) message with buttons to confirm or cancel.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { fs } from '../../../../shared/fs/fs'
import * as downloadPatterns from '../../../../shared/utilities/downloadPatterns'
import { ExtContext } from '../../../../shared/extensions'
import { workspaceUtils } from '../../../../shared'
import * as messages from '../../../../shared/utilities/messages'
import * as downloadPattern from '../../../../shared/utilities/downloadPatterns'
import * as wizardModule from '../../../../awsService/appBuilder/serverlessLand/wizard'

Expand Down Expand Up @@ -80,63 +81,75 @@ describe('createNewServerlessLandProject', () => {
})
})

function assertDownloadPatternCall(getPatternStub: sinon.SinonStub, mockConfig: any) {
const mockAssetName = 'test-project-sam-python.zip'
const serverlessLandOwner = 'aws-samples'
const serverlessLandRepo = 'serverless-patterns'
const mockLocation = vscode.Uri.joinPath(mockConfig.location, mockConfig.name)

assert(getPatternStub.calledOnce)
assert(getPatternStub.firstCall.args[0] === serverlessLandOwner)
assert(getPatternStub.firstCall.args[1] === serverlessLandRepo)
assert(getPatternStub.firstCall.args[2] === mockAssetName)
assert(getPatternStub.firstCall.args[3].toString() === mockLocation.toString())
assert(getPatternStub.firstCall.args[4] === true)
}

describe('downloadPatternCode', () => {
let sandbox: sinon.SinonSandbox
let getPatternStub: sinon.SinonStub
let mockConfig: any

beforeEach(function () {
sandbox = sinon.createSandbox()
getPatternStub = sandbox.stub(downloadPatterns, 'getPattern')
mockConfig = {
name: 'test-project',
location: vscode.Uri.file('/test'),
pattern: 'test-project-sam-python',
runtime: 'python',
iac: 'sam',
assetName: 'test-project-sam-python',
}
})
afterEach(function () {
sandbox.restore()
getPatternStub.restore()
})
const mockConfig = {
name: 'test-project',
location: vscode.Uri.file('/test'),
pattern: 'test-project-sam-python',
runtime: 'python',
iac: 'sam',
assetName: 'test-project-sam-python',
}
it('successfully downloads pattern code', async () => {
const mockAssetName = 'test-project-sam-python.zip'
const serverlessLandOwner = 'aws-samples'
const serverlessLandRepo = 'serverless-patterns'
const mockLocation = vscode.Uri.joinPath(mockConfig.location, mockConfig.name)

await downloadPatternCode(mockConfig, 'test-project-sam-python')
assert(getPatternStub.calledOnce)
assert(getPatternStub.firstCall.args[0] === serverlessLandOwner)
assert(getPatternStub.firstCall.args[1] === serverlessLandRepo)
assert(getPatternStub.firstCall.args[2] === mockAssetName)
assert(getPatternStub.firstCall.args[3].toString() === mockLocation.toString())
assert(getPatternStub.firstCall.args[4] === true)
})
it('handles download failure', async () => {
const mockAssetName = 'test-project-sam-python.zip'
const error = new Error('Download failed')
getPatternStub.rejects(error)
try {
await downloadPatternCode(mockConfig, mockAssetName)
assert.fail('Expected an error to be thrown')
} catch (err: any) {
assert.strictEqual(err.message, 'Failed to download pattern: Error: Download failed')
}
sandbox.stub(messages, 'handleOverwriteConflict').resolves(true)

await downloadPatternCode(mockConfig, mockConfig.assetName)
assertDownloadPatternCall(getPatternStub, mockConfig)
})
it('downloads pattern when directory exists and user confirms overwrite', async function () {
sandbox.stub(messages, 'handleOverwriteConflict').resolves(true)

await downloadPatternCode(mockConfig, mockConfig.assetName)
assertDownloadPatternCall(getPatternStub, mockConfig)
})
it('throws error when directory exists and user cancels overwrite', async function () {
const handleOverwriteStub = sandbox.stub(messages, 'handleOverwriteConflict')
handleOverwriteStub.rejects(new Error('Folder already exists: test-project'))

await assert.rejects(
() => downloadPatternCode(mockConfig, mockConfig.assetName),
/Folder already exists: test-project/
)
})
})

describe('openReadmeFile', () => {
let sandbox: sinon.SinonSandbox
let testsandbox: sinon.SinonSandbox
let spyExecuteCommand: sinon.SinonSpy

beforeEach(function () {
sandbox = sinon.createSandbox()
spyExecuteCommand = sandbox.spy(vscode.commands, 'executeCommand')
testsandbox = sinon.createSandbox()
spyExecuteCommand = testsandbox.spy(vscode.commands, 'executeCommand')
})

afterEach(function () {
sandbox.restore()
testsandbox.restore()
})
const mockConfig = {
name: 'test-project',
Expand All @@ -148,43 +161,43 @@ describe('openReadmeFile', () => {
}
it('successfully opens README file', async () => {
const mockReadmeUri = vscode.Uri.file('/test/README.md')
sandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri)
testsandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri)

sandbox.stub(fs, 'exists').resolves(true)
testsandbox.stub(fs, 'exists').resolves(true)

// When
await openReadmeFile(mockConfig)
// Then
sandbox.assert.calledWith(spyExecuteCommand, 'workbench.action.focusFirstEditorGroup')
sandbox.assert.calledWith(spyExecuteCommand, 'markdown.showPreview')
testsandbox.assert.calledWith(spyExecuteCommand, 'workbench.action.focusFirstEditorGroup')
testsandbox.assert.calledWith(spyExecuteCommand, 'markdown.showPreview')
})

it('handles missing README file', async () => {
const mockReadmeUri = vscode.Uri.file('/test/file.md')
sandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri)
testsandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri)

sandbox.stub(fs, 'exists').resolves(false)
testsandbox.stub(fs, 'exists').resolves(false)

// When
await openReadmeFile(mockConfig)
// Then
sandbox.assert.neverCalledWith(spyExecuteCommand, 'markdown.showPreview')
testsandbox.assert.neverCalledWith(spyExecuteCommand, 'markdown.showPreview')
assert.ok(true, 'Function should return without throwing error when README is not found')
})

it('handles error with opening README file', async () => {
const mockReadmeUri = vscode.Uri.file('/test/README.md')
sandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri)
testsandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri)

sandbox.stub(fs, 'exists').rejects(new Error('File system error'))
testsandbox.stub(fs, 'exists').rejects(new Error('File system error'))

// When
await assert.rejects(() => openReadmeFile(mockConfig), {
name: 'Error',
message: 'Error processing README file',
})
// Then
sandbox.assert.neverCalledWith(spyExecuteCommand, 'markdown.showPreview')
testsandbox.assert.neverCalledWith(spyExecuteCommand, 'markdown.showPreview')
})
})

Expand Down
Loading