// 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 diy import ( "context" "strings" "testing" "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" "gocloud.dev/blob/memblob" ) func TestLegacyReferenceStore_referencePaths(t *testing.T) { t.Parallel() bucket := memblob.OpenBucket(nil) store := newLegacyReferenceStore(bucket) ref, err := store.ParseReference("foo") require.NoError(t, err) assert.Equal(t, tokens.MustParseStackName("foo"), ref.Name()) assert.Equal(t, tokens.QName("foo"), ref.FullyQualifiedName()) assert.Equal(t, ".pulumi/stacks/foo", ref.StackBasePath()) assert.Equal(t, ".pulumi/history/foo", ref.HistoryDir()) assert.Equal(t, ".pulumi/backups/foo", ref.BackupDir()) } func TestProjectReferenceStore_referencePaths(t *testing.T) { t.Parallel() bucket := memblob.OpenBucket(nil) store := newProjectReferenceStore(bucket, func() *workspace.Project { return &workspace.Project{Name: "test"} }) ref, err := store.ParseReference("organization/myproject/mystack") require.NoError(t, err) assert.Equal(t, ".pulumi/stacks/myproject/mystack", ref.StackBasePath()) assert.Equal(t, ".pulumi/history/myproject/mystack", ref.HistoryDir()) assert.Equal(t, ".pulumi/backups/myproject/mystack", ref.BackupDir()) } func TestProjectReferenceStore_ParseReference(t *testing.T) { t.Parallel() bucket := memblob.OpenBucket(nil) store := newProjectReferenceStore(bucket, func() *workspace.Project { return &workspace.Project{Name: "currentProject"} }) tests := []struct { desc string give string fqname tokens.QName name string project tokens.Name str string }{ { desc: "simple", give: "foo", fqname: "organization/currentProject/foo", name: "foo", project: "currentProject", str: "foo", // truncated because project name is the same as current project }, { desc: "organization", give: "organization/foo", fqname: "organization/currentProject/foo", name: "foo", project: "currentProject", str: "foo", }, { desc: "fully qualified", give: "organization/project/foo", fqname: "organization/project/foo", name: "foo", project: "project", str: "organization/project/foo", // doesn't match current project }, } for _, tt := range tests { tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() ref, err := store.ParseReference(tt.give) require.NoError(t, err) assert.Equal(t, tt.fqname, ref.FullyQualifiedName()) assert.Equal(t, tokens.MustParseStackName(tt.name), ref.Name()) proj, has := ref.Project() assert.True(t, has) assert.Equal(t, tt.project, proj) assert.Equal(t, tt.str, ref.String()) }) } } func TestLegacyReferenceStore_ParseReference_errors(t *testing.T) { t.Parallel() bucket := memblob.OpenBucket(nil) store := newLegacyReferenceStore(bucket) tests := []struct { desc string give string }{ {desc: "empty", give: ""}, {desc: "invalid name", give: "foo/bar"}, {desc: "too many parts", give: "foo/bar/baz"}, { desc: "over 100 characters", give: strings.Repeat("a", 101), }, } for _, tt := range tests { tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() _, err := store.ParseReference(tt.give) assert.Error(t, err) // If we ever make error messages here more specific, // we can add assert.ErrorContains here. }) } } func TestProjectReferenceStore_ParseReference_errors(t *testing.T) { t.Parallel() bucket := memblob.OpenBucket(nil) store := newProjectReferenceStore(bucket, func() *workspace.Project { return nil // current project is not set }) tests := []struct { desc string give string wantErr string }{ { desc: "empty", wantErr: "must not be empty", }, { desc: "bad organization", give: "foo/bar/baz", wantErr: "organization name must be 'organization'", }, { desc: "long project name", give: "organization/" + strings.Repeat("a", 101) + "/foo", wantErr: "project names are limited to 100 characters", }, { desc: "long project stack name", give: "organization/foo/" + strings.Repeat("a", 101), wantErr: "a stack name cannot exceed 100 characters", }, { desc: "no current project", give: "organization/foo", wantErr: "pass the fully qualified name", }, { desc: "invalid project name", give: "organization/foo:bar/baz", wantErr: "may only contain alphanumeric", }, { desc: "invalid stack name", give: "organization/foo/baz:qux", wantErr: "may only contain alphanumeric", }, } for _, tt := range tests { tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() require.NotEmpty(t, tt.wantErr, "bad test case: wantErr must be non-empty") _, err := store.ParseReference(tt.give) assert.ErrorContains(t, err, tt.wantErr) }) } } func TestLegacyReferenceStore_ListReferences(t *testing.T) { t.Parallel() tests := []struct { desc string // List of file paths relative to the storage root // that should exist before ListReferences is called. files []string // List of fully-qualified stack names that should be returned // by ListReferences. want []tokens.QName }{ { desc: "empty", want: []tokens.QName{}, }, { desc: "json", files: []string{ ".pulumi/stacks/foo.json", }, want: []tokens.QName{"foo"}, }, { desc: "gzipped", files: []string{ ".pulumi/stacks/foo.json.gz", }, want: []tokens.QName{"foo"}, }, { desc: "multiple", files: []string{ ".pulumi/stacks/foo.json", ".pulumi/stacks/bar.json.gz", ".pulumi/stacks/baz.json", }, want: []tokens.QName{"bar", "baz", "foo"}, }, { desc: "extraneous directories", files: []string{ ".pulumi/stacks/foo.json", ".pulumi/stacks/bar.json/baz.json", // not a file }, want: []tokens.QName{"foo"}, }, } for _, tt := range tests { tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() bucket := memblob.OpenBucket(nil) store := newLegacyReferenceStore(bucket) ctx := context.Background() for _, f := range tt.files { require.NoError(t, bucket.WriteAll(ctx, f, []byte{}, nil)) } refs, err := store.ListReferences(ctx) require.NoError(t, err) got := make([]tokens.QName, len(refs)) for i, ref := range refs { got[i] = ref.FullyQualifiedName() } assert.Equal(t, tt.want, got) }) } } func TestProjectReferenceStore_List(t *testing.T) { t.Parallel() tests := []struct { desc string // List of file paths relative to the storage root // that should exist before ListReferences is called. files []string // List of fully-qualified stack names that should be returned // by ListReferences. stacks []tokens.QName // List of project names that should be returned by ListProjects. projects []tokens.Name }{ { desc: "empty", stacks: []tokens.QName{}, projects: nil, }, { desc: "json", files: []string{ ".pulumi/stacks/proj/foo.json", }, stacks: []tokens.QName{"organization/proj/foo"}, projects: []tokens.Name{"proj"}, }, { desc: "gzipped", files: []string{ ".pulumi/stacks/foo/bar.json.gz", }, stacks: []tokens.QName{"organization/foo/bar"}, projects: []tokens.Name{"foo"}, }, { desc: "multiple", files: []string{ ".pulumi/stacks/a/foo.json", ".pulumi/stacks/b/bar.json.gz", ".pulumi/stacks/c/baz.json", }, stacks: []tokens.QName{ "organization/a/foo", "organization/b/bar", "organization/c/baz", }, projects: []tokens.Name{"a", "b", "c"}, }, { desc: "extraneous files and directories", files: []string{ ".pulumi/stacks/a/foo.json", ".pulumi/stacks/foo.json", ".pulumi/stacks/bar/baz/qux.json", // nested too deep ".pulumi/stacks/a b/c.json", // bad project name }, stacks: []tokens.QName{"organization/a/foo"}, projects: []tokens.Name{"a", "bar"}, }, } for _, tt := range tests { tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() bucket := memblob.OpenBucket(nil) store := newProjectReferenceStore(bucket, func() *workspace.Project { return &workspace.Project{Name: "test"} }) ctx := context.Background() for _, f := range tt.files { require.NoError(t, bucket.WriteAll(ctx, f, []byte{}, nil)) } t.Run("Projects", func(t *testing.T) { t.Parallel() projects, err := store.ListProjects(ctx) require.NoError(t, err) assert.Equal(t, tt.projects, projects) }) t.Run("References", func(t *testing.T) { t.Parallel() refs, err := store.ListReferences(ctx) require.NoError(t, err) got := make([]tokens.QName, len(refs)) for i, ref := range refs { got[i] = ref.FullyQualifiedName() } assert.Equal(t, tt.stacks, got) }) }) } } func TestProjectReferenceStore_ProjectExists(t *testing.T) { t.Parallel() tests := []struct { desc string // List of file paths relative to the storage root // that should exist before ListReferences is called. files []string // Project name that should exist before ProjectExists is called. projectName string // Result that should be returned by ProjectExists. exist bool }{ { desc: "project exists", files: []string{ ".pulumi/stacks/a/foo.json", }, projectName: "a", exist: true, }, { desc: "project exists as empty directory", files: []string{ ".pulumi/stacks/a", }, projectName: "a", exist: false, }, { desc: "project does not exist", files: []string{ ".pulumi/stacks/a", }, projectName: "b", exist: false, }, { desc: "subproject exist", files: []string{ ".pulumi/stacks/b/a", // Project name exist, but as a subproject }, projectName: "a", exist: false, }, } for _, tt := range tests { tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() bucket := memblob.OpenBucket(nil) store := newProjectReferenceStore(bucket, func() *workspace.Project { return &workspace.Project{Name: "test"} }) ctx := context.Background() for _, f := range tt.files { require.NoError(t, bucket.WriteAll(ctx, f, []byte{}, nil)) } exist, err := store.ProjectExists(ctx, tt.projectName) assert.NoError(t, err) assert.Equal(t, tt.exist, exist) }) } }