mirror of https://github.com/pulumi/pulumi.git
1188 lines
29 KiB
Go
1188 lines
29 KiB
Go
// Copyright 2016-2023, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/pulumi/pulumi/pkg/v3/backend"
|
|
"github.com/pulumi/pulumi/pkg/v3/backend/display"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// mockBackendInstance sets the backendInstance for the test and cleans it up after.
|
|
func mockBackendInstance(t *testing.T, b backend.Backend) {
|
|
t.Cleanup(func() {
|
|
backendInstance = nil
|
|
})
|
|
backendInstance = b
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestFailInInteractiveWithoutYes(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
args := newArgs{
|
|
interactive: false,
|
|
yes: false,
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
stack: stackName,
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestFailIfProjectNameDoesNotMatch(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
args := newArgs{
|
|
interactive: false,
|
|
yes: true,
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
stack: "org/projectA/stack",
|
|
name: "projectB",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.ErrorContains(t, err, "project name (--name projectB) "+
|
|
"and stack reference project name (--stack projectA) must be the same")
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestCreatingStackWithArgsSpecifiedOrgName(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
orgStackName := fmt.Sprintf("%s/%s", currentUser(t), stackName)
|
|
|
|
args := newArgs{
|
|
interactive: false,
|
|
yes: true,
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
stack: orgStackName,
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, stackName, loadStackName(t))
|
|
removeStack(t, tempdir, stackName)
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestCreatingStackWithPromptedOrgName(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
uniqueProjectName := filepath.Base(tempdir)
|
|
orgStackName := fmt.Sprintf("%s/%s", currentUser(t), stackName)
|
|
|
|
args := newArgs{
|
|
interactive: true,
|
|
prompt: promptMock(uniqueProjectName, orgStackName),
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, stackName, loadStackName(t))
|
|
removeStack(t, tempdir, stackName)
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestCreatingStackWithArgsSpecifiedFullNameSucceeds(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
// the project name and the project name in the stack name must match
|
|
uniqueProjectName := filepath.Base(tempdir)
|
|
fullStackName := fmt.Sprintf("%s/%s/%s", currentUser(t), uniqueProjectName, stackName)
|
|
|
|
args := newArgs{
|
|
interactive: false,
|
|
yes: true,
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
stack: fullStackName,
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, stackName, loadStackName(t))
|
|
removeStack(t, tempdir, stackName)
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestCreatingProjectWithArgsSpecifiedName(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
uniqueProjectName := filepath.Base(tempdir) + "test"
|
|
|
|
args := newArgs{
|
|
interactive: false,
|
|
yes: true,
|
|
name: uniqueProjectName,
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
stack: stackName,
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
removeStack(t, tempdir, stackName)
|
|
|
|
proj := loadProject(t, tempdir)
|
|
assert.Equal(t, uniqueProjectName, proj.Name.String())
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestCreatingProjectWithPromptedName(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
uniqueProjectName := filepath.Base(tempdir) + "test"
|
|
|
|
args := newArgs{
|
|
interactive: true,
|
|
prompt: promptMock(uniqueProjectName, stackName),
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
removeStack(t, tempdir, stackName)
|
|
|
|
proj := loadProject(t, tempdir)
|
|
assert.Equal(t, uniqueProjectName, proj.Name.String())
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process, mocks backendInstance
|
|
func TestCreatingProjectWithExistingArgsSpecifiedNameFails(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
mockBackendInstance(t, &backend.MockBackend{
|
|
DoesProjectExistF: func(ctx context.Context, org string, name string) (bool, error) {
|
|
return name == projectName, nil
|
|
},
|
|
})
|
|
|
|
args := newArgs{
|
|
interactive: false,
|
|
yes: true,
|
|
name: projectName,
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.ErrorContains(t, err, "project with this name already exists")
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process, mocks backendInstance
|
|
func TestCreatingProjectWithExistingPromptedNameFails(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
mockBackendInstance(t, &backend.MockBackend{
|
|
DoesProjectExistF: func(ctx context.Context, org string, name string) (bool, error) {
|
|
return name == projectName, nil
|
|
},
|
|
})
|
|
|
|
args := newArgs{
|
|
interactive: true,
|
|
prompt: promptMock(projectName, ""),
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.ErrorContains(t, err, "Try again")
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process, mocks backendInstance
|
|
func TestGeneratingProjectWithExistingArgsSpecifiedNameSucceeds(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
mockBackendInstance(t, &backend.MockBackend{
|
|
DoesProjectExistF: func(ctx context.Context, org string, name string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
})
|
|
|
|
// Generate-only command is not creating any stacks, so don't bother with with the name uniqueness check.
|
|
args := newArgs{
|
|
generateOnly: true,
|
|
interactive: false,
|
|
yes: true,
|
|
name: projectName,
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
proj := loadProject(t, tempdir)
|
|
assert.Equal(t, projectName, proj.Name.String())
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process, mocks backendInstance
|
|
func TestGeneratingProjectWithExistingPromptedNameSucceeds(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
mockBackendInstance(t, &backend.MockBackend{
|
|
DoesProjectExistF: func(ctx context.Context, org string, name string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
})
|
|
|
|
// Generate-only command is not creating any stacks, so don't bother with with the name uniqueness check.
|
|
args := newArgs{
|
|
generateOnly: true,
|
|
interactive: true,
|
|
prompt: promptMock(projectName, ""),
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
proj := loadProject(t, tempdir)
|
|
assert.Equal(t, projectName, proj.Name.String())
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestCreatingProjectWithEmptyConfig(t *testing.T) {
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/4081
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
uniqueProjectName := filepath.Base(tempdir) + "test"
|
|
|
|
prompt := func(yes bool, valueType string, defaultValue string, secret bool,
|
|
isValidFn func(value string) error, opts display.Options,
|
|
) (string, error) {
|
|
if strings.Contains(valueType, "(aws:region)") {
|
|
return "", nil
|
|
}
|
|
return defaultValue, nil
|
|
}
|
|
|
|
args := newArgs{
|
|
name: uniqueProjectName,
|
|
stack: stackName,
|
|
interactive: true,
|
|
prompt: prompt,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "aws-typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
require.NoError(t, err)
|
|
|
|
proj := loadProject(t, tempdir)
|
|
projStack, err := workspace.LoadProjectStack(proj, filepath.Join(tempdir, "Pulumi."+stackName+".yaml"))
|
|
require.NoError(t, err)
|
|
|
|
assert.NotContains(t, projStack.Config, config.MustMakeKey("aws", "region"))
|
|
|
|
removeStack(t, tempdir, stackName)
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process, mocks backendInstance
|
|
func TestGeneratingProjectWithInvalidArgsSpecifiedNameFails(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
mockBackendInstance(t, &backend.MockBackend{
|
|
DoesProjectExistF: func(ctx context.Context, org string, name string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
})
|
|
|
|
// Generate-only command is not creating any stacks, so don't bother with with the name uniqueness check.
|
|
args := newArgs{
|
|
generateOnly: true,
|
|
interactive: false,
|
|
yes: true,
|
|
name: "not#valid",
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.ErrorContains(t, err, "project names may only contain")
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process, mocks backendInstance
|
|
func TestGeneratingProjectWithInvalidPromptedNameFails(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
mockBackendInstance(t, &backend.MockBackend{
|
|
DoesProjectExistF: func(ctx context.Context, org string, name string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
})
|
|
|
|
// Generate-only command is not creating any stacks, so don't bother with with the name uniqueness check.
|
|
err := runNew(context.Background(), newArgs{
|
|
generateOnly: true,
|
|
interactive: true,
|
|
prompt: promptMock("not#valid", ""),
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
})
|
|
assert.ErrorContains(t, err, "project names may only contain")
|
|
|
|
err = runNew(context.Background(), newArgs{
|
|
generateOnly: true,
|
|
interactive: true,
|
|
prompt: promptMock("", ""),
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "typescript",
|
|
})
|
|
assert.ErrorContains(t, err, "project names may not be empty")
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestInvalidTemplateName(t *testing.T) {
|
|
skipIfShortOrNoPulumiAccessToken(t)
|
|
|
|
t.Run("NoTemplateSpecified", func(t *testing.T) {
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
args := newArgs{
|
|
chooseTemplate: chooseTemplate,
|
|
interactive: false,
|
|
yes: true,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "",
|
|
templateMode: true,
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.ErrorContains(t, err, "no template selected")
|
|
})
|
|
|
|
t.Run("RemoteTemplateNotFound", func(t *testing.T) {
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
// A template that will never exist.
|
|
template := "this-is-not-the-template-youre-looking-for"
|
|
|
|
args := newArgs{
|
|
interactive: false,
|
|
yes: true,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: template,
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.ErrorContains(t, err, "not found")
|
|
})
|
|
|
|
t.Run("LocalTemplateNotFound", func(t *testing.T) {
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
// A template that will never exist remotely.
|
|
template := "this-is-not-the-template-youre-looking-for"
|
|
|
|
args := newArgs{
|
|
generateOnly: true,
|
|
offline: true,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: template,
|
|
yes: true,
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.ErrorContains(t, err, "not found")
|
|
})
|
|
}
|
|
|
|
func TestParseConfigSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
Array []string
|
|
Path bool
|
|
Expected config.Map
|
|
}{
|
|
{
|
|
Array: []string{},
|
|
Expected: config.Map{},
|
|
},
|
|
{
|
|
Array: []string{"my:testKey"},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue(""),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{"my:testKey="},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue(""),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{"my:testKey=testValue"},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("testValue"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{"my:testKey=test=Value"},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("test=Value"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
"my:testKey=testValue",
|
|
"my:testKey=rewritten",
|
|
},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("rewritten"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
"my:testKey=testValue",
|
|
},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("testValue"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
"my:test.Key=testValue",
|
|
},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "test.Key"): config.NewValue("testValue"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
"my:testKey=testValue",
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("testValue"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
"my:0=testValue",
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "0"): config.NewValue("testValue"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
"my:true=testValue",
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "true"): config.NewValue("testValue"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:["test.Key"]=testValue`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "test.Key"): config.NewValue("testValue"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:outer.inner=value`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "outer"): config.NewObjectValue(`{"inner":"value"}`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:outer.inner.nested=value`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "outer"): config.NewObjectValue(`{"inner":{"nested":"value"}}`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:name[0]=value`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "name"): config.NewObjectValue(`["value"]`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:name[0][0]=value`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "name"): config.NewObjectValue(`[["value"]]`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:servers[0].name=foo`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "servers"): config.NewObjectValue(`[{"name":"foo"}]`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:testKey=false`,
|
|
},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("false"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:testKey=true`,
|
|
},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("true"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:testKey=10`,
|
|
},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("10"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:testKey=-1`,
|
|
},
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewValue("-1"),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:testKey[0]=false`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewObjectValue(`[false]`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:testKey[0]=true`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewObjectValue(`[true]`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:testKey[0]=10`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewObjectValue(`[10]`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:testKey[0]=-1`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "testKey"): config.NewObjectValue(`[-1]`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:names[0]=a`,
|
|
`my:names[1]=b`,
|
|
`my:names[2]=c`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "names"): config.NewObjectValue(`["a","b","c"]`),
|
|
},
|
|
},
|
|
{
|
|
Array: []string{
|
|
`my:names[0]=a`,
|
|
`my:names[1]=b`,
|
|
`my:names[2]=c`,
|
|
`my:names[0]=rewritten`,
|
|
},
|
|
Path: true,
|
|
Expected: config.Map{
|
|
config.MustMakeKey("my", "names"): config.NewObjectValue(`["rewritten","b","c"]`),
|
|
},
|
|
},
|
|
}
|
|
|
|
//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(fmt.Sprintf("%v", test), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
actual, err := parseConfig(test.Array, test.Path)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, test.Expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetFail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
Array []string
|
|
Expected config.Map
|
|
}{
|
|
{
|
|
Array: []string{`my:[""]=value`},
|
|
},
|
|
{
|
|
Array: []string{"my:[0]=value"},
|
|
},
|
|
{
|
|
Array: []string{`my:name[-1]=value`},
|
|
},
|
|
{
|
|
Array: []string{`my:key.secure=value`},
|
|
},
|
|
}
|
|
|
|
//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(fmt.Sprintf("%v", test), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := parseConfig(test.Array, true /*path*/)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestErrorIfNotEmptyDirectory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
desc string
|
|
files []string
|
|
dirs []string
|
|
ok bool
|
|
}{
|
|
{
|
|
desc: "empty",
|
|
ok: true,
|
|
},
|
|
{
|
|
desc: "non-empty",
|
|
files: []string{"foo"},
|
|
dirs: []string{"bar"},
|
|
ok: false,
|
|
},
|
|
{
|
|
desc: "empty git repository",
|
|
dirs: []string{".git"},
|
|
ok: true,
|
|
},
|
|
{
|
|
desc: "non-empty git repository",
|
|
dirs: []string{".git"},
|
|
files: []string{".gitignore"},
|
|
ok: false,
|
|
},
|
|
{
|
|
desc: "every VCS",
|
|
dirs: []string{".git", ".hg", ".bzr"},
|
|
ok: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := t.TempDir()
|
|
|
|
// Fill test directory with files and directories
|
|
// requested by the test case.
|
|
for _, name := range tt.dirs {
|
|
err := os.MkdirAll(filepath.Join(path, name), 0o1700)
|
|
require.NoError(t, err)
|
|
}
|
|
for _, name := range tt.files {
|
|
err := os.WriteFile(filepath.Join(path, name), nil /* body */, 0o600)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
err := errorIfNotEmptyDirectory(path)
|
|
if tt.ok {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func tempProjectDir(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
dir := filepath.Join(t.TempDir(), genUniqueName(t))
|
|
require.NoError(t, os.MkdirAll(dir, 0o700))
|
|
return dir
|
|
}
|
|
|
|
func genUniqueName(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
var bs [8]byte
|
|
_, err := rand.Read(bs[:])
|
|
require.NoError(t, err)
|
|
|
|
return "test-" + hex.EncodeToString(bs[:])
|
|
}
|
|
|
|
func TestValidateStackRefAndProjectName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
b := &backend.MockBackend{
|
|
ParseStackReferenceF: func(s string) (backend.StackReference, error) {
|
|
parts := strings.Split(s, "/")
|
|
switch len(parts) {
|
|
case 1:
|
|
return &backend.MockStackReference{
|
|
NameV: tokens.MustParseStackName(parts[0]),
|
|
}, nil
|
|
case 2:
|
|
return &backend.MockStackReference{
|
|
ProjectV: tokens.Name(parts[0]),
|
|
NameV: tokens.MustParseStackName(parts[1]),
|
|
}, nil
|
|
case 3:
|
|
return &backend.MockStackReference{
|
|
ProjectV: tokens.Name(parts[1]),
|
|
NameV: tokens.MustParseStackName(parts[2]),
|
|
}, nil
|
|
|
|
default:
|
|
return nil, errors.Errorf("invalid stack reference %q", s)
|
|
}
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
projectName string
|
|
stackRef string
|
|
valid bool
|
|
}{
|
|
{
|
|
projectName: "foo",
|
|
stackRef: "foo",
|
|
valid: true,
|
|
},
|
|
{
|
|
projectName: "fooo",
|
|
stackRef: "org/foo/dev",
|
|
valid: false,
|
|
},
|
|
{
|
|
projectName: "",
|
|
stackRef: "org/foo/dev",
|
|
valid: true,
|
|
},
|
|
{
|
|
projectName: "foo",
|
|
stackRef: "",
|
|
valid: true,
|
|
},
|
|
{
|
|
projectName: "foo",
|
|
stackRef: "org/foo/dev",
|
|
valid: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(fmt.Sprintf("project=%q/stackRef=%q", tt.projectName, tt.stackRef), func(t *testing.T) {
|
|
t.Parallel()
|
|
err := compareStackProjectName(b, tt.stackRef, tt.projectName)
|
|
if tt.valid {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProjectExists(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
b := &backend.MockBackend{
|
|
DoesProjectExistF: func(ctx context.Context, orgName string, projectName string) (bool, error) {
|
|
type Org string
|
|
type ProjectName string
|
|
projects := map[Org]map[ProjectName]struct{}{
|
|
"moolumi": {
|
|
"my-moolumi-project": {},
|
|
},
|
|
"pulumi": {},
|
|
}
|
|
orgProjects, ok := projects[Org(orgName)]
|
|
if !ok {
|
|
return false, fmt.Errorf("org %s not found", orgName)
|
|
}
|
|
_, exists := orgProjects[ProjectName(projectName)]
|
|
return exists, nil
|
|
},
|
|
}
|
|
|
|
// Table Test
|
|
type Project struct {
|
|
orgName string
|
|
projectName string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
give Project
|
|
hasErr bool
|
|
}{
|
|
{
|
|
name: "project exists",
|
|
give: Project{
|
|
projectName: "my-moolumi-project",
|
|
orgName: "moolumi",
|
|
},
|
|
hasErr: true,
|
|
},
|
|
{
|
|
name: "project exists in another org",
|
|
give: Project{
|
|
projectName: "my-moolumi-project",
|
|
orgName: "pulumi",
|
|
},
|
|
hasErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
err := validateProjectName(ctx, b, tt.give.orgName, tt.give.projectName, false /* generateOnly */, display.Options{})
|
|
if tt.hasErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestGenerateOnlyProjectCheck(t *testing.T) {
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/13527, make sure various combinations of
|
|
// project name and stack work when generateOnly is set (thus skipping backend checks).
|
|
|
|
cases := []struct {
|
|
name string
|
|
stack string
|
|
}{
|
|
{name: "mismatched project name", stack: "organization/boom/stack"},
|
|
{name: "fully qualified stack name", stack: "organization/project/stack"},
|
|
{name: "org qualified stack name", stack: "organization/stack"},
|
|
{name: "unqualified stack name", stack: "stack"},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
args := newArgs{
|
|
generateOnly: true,
|
|
interactive: false,
|
|
yes: true,
|
|
prompt: promptForValue,
|
|
secretsProvider: "default",
|
|
stack: tt.stack,
|
|
name: "project",
|
|
templateNameOrURL: "typescript",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPulumiNewConflictingProject(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
b := &backend.MockBackend{
|
|
DoesProjectExistF: func(ctx context.Context, orgName string, projectName string) (bool, error) {
|
|
if projectName == "existing-project-name" {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
assert.NoError(t,
|
|
validateProjectNameInternal(
|
|
context.Background(), b, "moolumi", "some-project-name", false /* generateOnly */, display.Options{},
|
|
func(s string) error {
|
|
assert.Fail(t, "this should not be called as this is a not a duplicate project name")
|
|
return nil
|
|
},
|
|
))
|
|
|
|
var called bool
|
|
assert.NoError(t,
|
|
validateProjectNameInternal(
|
|
context.Background(), b, "moolumi", "existing-project-name", false /* generateOnly */, display.Options{},
|
|
func(s string) error {
|
|
called = true
|
|
return nil
|
|
},
|
|
))
|
|
assert.Truef(t, called, "expected resolution to be called with duplicate name")
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestPulumiNewSetsTemplateTag(t *testing.T) {
|
|
tests := []struct {
|
|
argument string
|
|
prompted string
|
|
expected string
|
|
}{
|
|
{
|
|
"typescript",
|
|
"",
|
|
"typescript",
|
|
},
|
|
{
|
|
"https://github.com/pulumi/templates/tree/master/yaml?foo=bar",
|
|
"",
|
|
"https://github.com/pulumi/templates/tree/master/yaml",
|
|
},
|
|
{
|
|
"",
|
|
"python",
|
|
"python",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
name := tt.argument
|
|
if name == "" {
|
|
name = tt.prompted
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
uniqueProjectName := filepath.Base(tempdir) + "test"
|
|
|
|
chooseTemplateMock := func(templates []workspace.Template, opts display.Options,
|
|
) (workspace.Template, error) {
|
|
for _, template := range templates {
|
|
if template.Name == tt.prompted {
|
|
return template, nil
|
|
}
|
|
}
|
|
return workspace.Template{}, errors.New("template not found")
|
|
}
|
|
|
|
runtimeOptionsMock := func(ctx *plugin.Context, info *workspace.ProjectRuntimeInfo,
|
|
main string, opts display.Options, interactive, yes bool, prompt promptForValueFunc,
|
|
) (map[string]interface{}, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
args := newArgs{
|
|
interactive: tt.prompted != "",
|
|
generateOnly: true,
|
|
yes: true,
|
|
templateMode: true,
|
|
name: projectName,
|
|
prompt: promptMock(uniqueProjectName, stackName),
|
|
promptRuntimeOptions: runtimeOptionsMock,
|
|
chooseTemplate: chooseTemplateMock,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: tt.argument,
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
proj := loadProject(t, tempdir)
|
|
require.NoError(t, err)
|
|
tagsValue, has := proj.Config[apitype.PulumiTagsConfigKey]
|
|
assert.True(t, has)
|
|
tagsObject, ok := tagsValue.Value.(map[string]interface{})
|
|
assert.True(t, ok)
|
|
assert.Equal(t, tt.expected, tagsObject[apitype.ProjectTemplateTag])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizeTemplate(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"https://user:pass@example.com/path?param=value", "https://example.com/path"},
|
|
{"https://user:pass@example.com", "https://example.com"},
|
|
{"https://example.com/path?param=value", "https://example.com/path"},
|
|
{"ssh://user@hostname/project/repo", "ssh://hostname/project/repo"},
|
|
{"typescript", "typescript"},
|
|
{"aws-typescript", "aws-typescript"},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
t.Parallel()
|
|
result := sanitizeTemplate(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestPulumiPromptRuntimeOptions(t *testing.T) {
|
|
tempdir := tempProjectDir(t)
|
|
chdir(t, tempdir)
|
|
|
|
runtimeOptionsMock := func(ctx *plugin.Context, info *workspace.ProjectRuntimeInfo,
|
|
main string, opts display.Options, interactive, yes bool, prompt promptForValueFunc,
|
|
) (map[string]interface{}, error) {
|
|
return map[string]interface{}{"someOption": "someValue"}, nil
|
|
}
|
|
|
|
args := newArgs{
|
|
interactive: false,
|
|
generateOnly: true,
|
|
yes: true,
|
|
templateMode: true,
|
|
name: projectName,
|
|
prompt: promptForValue,
|
|
promptRuntimeOptions: runtimeOptionsMock,
|
|
secretsProvider: "default",
|
|
templateNameOrURL: "python",
|
|
}
|
|
|
|
err := runNew(context.Background(), args)
|
|
assert.NoError(t, err)
|
|
|
|
require.NoError(t, err)
|
|
proj := loadProject(t, tempdir)
|
|
require.Equal(t, 1, len(proj.Runtime.Options()))
|
|
require.Equal(t, "someValue", proj.Runtime.Options()["someOption"])
|
|
}
|