You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/servers/controller/handlers/projects/service_test.go

459 lines
14 KiB

package projects_test
import (
"context"
"fmt"
"strings"
"testing"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/hashicorp/watchtower/internal/db"
pb "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/scopes"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/projects"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/stretchr/testify/assert"
)
func createDefaultProjectAndRepo(t *testing.T) (*iam.Scope, *iam.Repository) {
t.Helper()
cleanup, conn, _ := db.TestSetup(t, "postgres")
t.Cleanup(func() {
conn.Close()
cleanup()
})
rw := db.New(conn)
wrap := db.TestWrapper(t)
repo, err := iam.NewRepository(rw, rw, wrap)
assert.Nil(t, err, "Unable to create new repo")
// Create a default org and project for our tests.
o, err := iam.NewOrganization(iam.WithName("default"))
if err != nil {
t.Fatalf("Could not get new org: %v", err)
}
oRes, err := repo.CreateScope(context.Background(), o)
if err != nil {
t.Fatalf("Could not create org scope: %v", err)
}
p, err := iam.NewProject(oRes.GetPublicId(), iam.WithName("default"), iam.WithDescription("default"))
if err != nil {
t.Fatalf("Could not get new project: %v", err)
}
pRes, err := repo.CreateScope(context.Background(), p)
if err != nil {
t.Fatalf("Could not create project scope: %v", err)
}
return pRes, repo
}
func TestGet(t *testing.T) {
proj, repo := createDefaultProjectAndRepo(t)
toMerge := &pbs.GetProjectRequest{
OrgId: proj.GetParentId(),
Id: proj.GetPublicId(),
}
pProject := &pb.Project{
Id: proj.GetPublicId(),
Name: &wrappers.StringValue{Value: proj.GetName()},
Description: &wrappers.StringValue{Value: proj.GetDescription()},
CreatedTime: proj.CreateTime.GetTimestamp(),
UpdatedTime: proj.UpdateTime.GetTimestamp(),
}
cases := []struct {
name string
req *pbs.GetProjectRequest
res *pbs.GetProjectResponse
errCode codes.Code
}{
{
name: "Get an Existing Project",
req: &pbs.GetProjectRequest{Id: proj.GetPublicId()},
res: &pbs.GetProjectResponse{Item: pProject},
errCode: codes.OK,
},
{
name: "Get a non existant Host Catalog",
req: &pbs.GetProjectRequest{Id: "p_DoesntExis"},
res: nil,
// This will be fixed with PR 42
errCode: codes.NotFound,
},
{
name: "Wrong id prefix",
req: &pbs.GetProjectRequest{Id: "j_1234567890"},
res: nil,
// This will be fixed with PR 42
errCode: codes.InvalidArgument,
},
{
name: "space in id",
req: &pbs.GetProjectRequest{Id: "p_1 23456789"},
res: nil,
// This will be fixed with PR 42
errCode: codes.InvalidArgument,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
req := proto.Clone(toMerge).(*pbs.GetProjectRequest)
proto.Merge(req, tc.req)
s := projects.NewService(repo)
got, gErr := s.GetProject(context.Background(), req)
assert.Equal(tc.errCode, status.Code(gErr), "GetProject(%+v) got error %v, wanted %v", req, gErr, tc.errCode)
assert.True(proto.Equal(got, tc.res), "GetProject(%q) got response %q, wanted %q", req, got, tc.res)
})
}
}
func TestCreate(t *testing.T) {
defaultProj, repo := createDefaultProjectAndRepo(t)
defaultProjCreated, err := ptypes.Timestamp(defaultProj.GetCreateTime().GetTimestamp())
if err != nil {
t.Fatalf("Error converting proto to timestamp: %v", err)
}
toMerge := &pbs.CreateProjectRequest{
OrgId: defaultProj.GetParentId(),
}
cases := []struct {
name string
req *pbs.CreateProjectRequest
res *pbs.CreateProjectResponse
errCode codes.Code
}{
{
name: "Create a valid Project",
req: &pbs.CreateProjectRequest{Item: &pb.Project{
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
}},
res: &pbs.CreateProjectResponse{
Uri: fmt.Sprintf("orgs/%s/projects/p_", defaultProj.GetParentId()),
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
},
},
errCode: codes.OK,
},
{
name: "Can't specify Id",
req: &pbs.CreateProjectRequest{Item: &pb.Project{
Id: "not allowed to be set",
}},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Can't specify Created Time",
req: &pbs.CreateProjectRequest{Item: &pb.Project{
CreatedTime: ptypes.TimestampNow(),
}},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Can't specify Update Time",
req: &pbs.CreateProjectRequest{Item: &pb.Project{
UpdatedTime: ptypes.TimestampNow(),
}},
res: nil,
errCode: codes.InvalidArgument,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
req := proto.Clone(toMerge).(*pbs.CreateProjectRequest)
proto.Merge(req, tc.req)
s := projects.NewService(repo)
got, gErr := s.CreateProject(context.Background(), req)
assert.Equal(tc.errCode, status.Code(gErr), "CreateProject(%+v) got error %v, wanted %v", req, gErr, tc.errCode)
if got != nil {
strings.HasPrefix(got.GetUri(), tc.res.Uri)
strings.HasPrefix(got.GetItem().GetId(), "p_")
gotCreateTime, err := ptypes.Timestamp(got.GetItem().GetCreatedTime())
if err != nil {
t.Fatalf("Error converting proto to timestamp: %v", err)
}
gotUpdateTime, err := ptypes.Timestamp(got.GetItem().GetUpdatedTime())
if err != nil {
t.Fatalf("Error converting proto to timestamp: %v", err)
}
// Verify it is a project created after the test setup's default project
assert.True(gotCreateTime.After(defaultProjCreated), "New project should have been created after default project. Was created %v, which is after %v", gotCreateTime, defaultProjCreated)
assert.True(gotUpdateTime.After(defaultProjCreated), "New project should have been updated after default project. Was updated %v, which is after %v", gotUpdateTime, defaultProjCreated)
// Clear all values which are hard to compare against.
got.Uri, tc.res.Uri = "", ""
got.Item.Id, tc.res.Item.Id = "", ""
got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil
}
assert.True(proto.Equal(got, tc.res), "CreateProject(%q) got response %q, wanted %q", req, got, tc.res)
})
}
}
func TestUpdate(t *testing.T) {
proj, repo := createDefaultProjectAndRepo(t)
tested := projects.NewService(repo)
var err error
resetProject := func() {
if proj, _, err = repo.UpdateScope(context.Background(), proj, []string{"Name", "Description"}); err != nil {
t.Fatalf("Failed to reset the project")
}
}
projCreated, err := ptypes.Timestamp(proj.GetCreateTime().GetTimestamp())
if err != nil {
t.Fatalf("Error converting proto to timestamp: %v", err)
}
toMerge := &pbs.UpdateProjectRequest{
OrgId: proj.GetParentId(),
Id: proj.GetPublicId(),
}
cases := []struct {
name string
req *pbs.UpdateProjectRequest
res *pbs.UpdateProjectResponse
errCode codes.Code
}{
{
name: "Update an Existing Project",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name", "description"},
},
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
},
},
res: &pbs.UpdateProjectResponse{
Item: &pb.Project{
Id: proj.GetPublicId(),
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
CreatedTime: proj.GetCreateTime().GetTimestamp(),
},
},
errCode: codes.OK,
},
{
name: "Multiple Paths in single string",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name,description"},
},
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
},
},
res: &pbs.UpdateProjectResponse{
Item: &pb.Project{
Id: proj.GetPublicId(),
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
CreatedTime: proj.GetCreateTime().GetTimestamp(),
},
},
errCode: codes.OK,
},
{
name: "No Update Mask Is Invalid Argument",
req: &pbs.UpdateProjectRequest{
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "updated name"},
Description: &wrappers.StringValue{Value: "updated desc"},
},
},
errCode: codes.InvalidArgument,
},
{
name: "No Paths in Mask Is Invalid Argument",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{}},
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "updated name"},
Description: &wrappers.StringValue{Value: "updated desc"},
},
},
errCode: codes.InvalidArgument,
},
{
name: "Only non-existant paths in Mask Is Invalid Argument",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}},
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "updated name"},
Description: &wrappers.StringValue{Value: "updated desc"},
},
},
errCode: codes.InvalidArgument,
},
{
name: "Unset Name",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name"},
},
Item: &pb.Project{
Description: &wrappers.StringValue{Value: "ignored"},
},
},
res: &pbs.UpdateProjectResponse{
Item: &pb.Project{
Id: proj.GetPublicId(),
Description: &wrappers.StringValue{Value: "default"},
CreatedTime: proj.GetCreateTime().GetTimestamp(),
},
},
errCode: codes.OK,
},
{
name: "Update Only Name",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name"},
},
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "updated"},
Description: &wrappers.StringValue{Value: "ignored"},
},
},
res: &pbs.UpdateProjectResponse{
Item: &pb.Project{
Id: proj.GetPublicId(),
Name: &wrappers.StringValue{Value: "updated"},
Description: &wrappers.StringValue{Value: "default"},
CreatedTime: proj.GetCreateTime().GetTimestamp(),
},
},
errCode: codes.OK,
},
{
name: "Update Only Description",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"description"},
},
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "ignored"},
Description: &wrappers.StringValue{Value: "notignored"},
},
},
res: &pbs.UpdateProjectResponse{
Item: &pb.Project{
Id: proj.GetPublicId(),
Name: &wrappers.StringValue{Value: "default"},
Description: &wrappers.StringValue{Value: "notignored"},
CreatedTime: proj.GetCreateTime().GetTimestamp(),
},
},
errCode: codes.OK,
},
{
name: "Update a Non Existing Project",
req: &pbs.UpdateProjectRequest{
Id: "p_DoesntExis",
UpdateMask: &field_mask.FieldMask{
Paths: []string{"description"},
},
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
},
},
errCode: codes.Unknown,
},
{
name: "Cant change Id",
req: &pbs.UpdateProjectRequest{
Id: proj.GetPublicId(),
UpdateMask: &field_mask.FieldMask{
Paths: []string{"id"},
},
Item: &pb.Project{
Id: "p_somethinge",
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "new desc"},
}},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Cant specify Created Time",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"created_time"},
},
Item: &pb.Project{
CreatedTime: ptypes.TimestampNow(),
},
},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Cant specify Updated Time",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"updated_time"},
},
Item: &pb.Project{
UpdatedTime: ptypes.TimestampNow(),
},
},
res: nil,
errCode: codes.InvalidArgument,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
defer resetProject()
assert := assert.New(t)
req := proto.Clone(toMerge).(*pbs.UpdateProjectRequest)
proto.Merge(req, tc.req)
got, gErr := tested.UpdateProject(context.Background(), req)
assert.Equal(tc.errCode, status.Code(gErr), "UpdateProject(%+v) got error %v, wanted %v", req, gErr, tc.errCode)
if got != nil {
assert.NotNilf(tc.res, "Expected UpdateProject response to be nil, but was %v", got)
gotUpdateTime, err := ptypes.Timestamp(got.GetItem().GetUpdatedTime())
if err != nil {
t.Fatalf("Error converting proto to timestamp: %v", err)
}
// Verify it is a project updated after it was created
// TODO: This is currently failing.
//assert.True(gotUpdateTime.After(projCreated), "Updated project should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, projCreated)
_ = gotUpdateTime
_ = projCreated
// Clear all values which are hard to compare against.
got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil
}
assert.True(proto.Equal(got, tc.res), "UpdateProject(%q) got response %q, wanted %q", req, got, tc.res)
})
}
}