Files
coder/enterprise/coderd/organizations_test.go
Steven Masley 7ea1a4c686 chore: protect organization endpoints with license (#14001)
* chore: move multi-org endpoints into enterprise directory

All multi-organization features are gated behind "premium" licenses. Enterprise licenses can no longer
access organization CRUD.
2024-07-25 16:07:53 -05:00

606 lines
19 KiB
Go

package coderd_test
import (
"bytes"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
)
func TestMultiOrgFetch(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
makeOrgs := []string{"foo", "bar", "baz"}
for _, name := range makeOrgs {
_, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: name,
DisplayName: name,
})
require.NoError(t, err)
}
//nolint:gocritic // using the owner intentionally since only they can make orgs
myOrgs, err := client.OrganizationsByUser(ctx, codersdk.Me)
require.NoError(t, err)
require.NotNil(t, myOrgs)
require.Len(t, myOrgs, len(makeOrgs)+1)
orgs, err := client.Organizations(ctx)
require.NoError(t, err)
require.NotNil(t, orgs)
require.ElementsMatch(t, myOrgs, orgs)
}
func TestOrganizationsByUser(t *testing.T) {
t.Parallel()
t.Run("IsDefault", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // owner is required to make orgs
orgs, err := client.OrganizationsByUser(ctx, codersdk.Me)
require.NoError(t, err)
require.NotNil(t, orgs)
require.Len(t, orgs, 1)
require.True(t, orgs[0].IsDefault, "first org is always default")
// Make an extra org, and it should not be defaulted.
notDefault, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "another",
DisplayName: "Another",
})
require.NoError(t, err)
require.False(t, notDefault.IsDefault, "only 1 default org allowed")
})
t.Run("NoMember", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, first := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // owner is required to make orgs
org, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "another",
DisplayName: "Another",
})
require.NoError(t, err)
_, err = other.OrganizationByUserAndName(ctx, codersdk.Me, org.Name)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
}
func TestAddOrganizationMembers(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
_, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitMedium)
//nolint:gocritic // must be an owner, only owners can create orgs
otherOrg, err := ownerClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "Other",
DisplayName: "",
Description: "",
Icon: "",
})
require.NoError(t, err, "create another organization")
inv, root := clitest.New(t, "organization", "members", "add", "-O", otherOrg.ID.String(), user.Username)
//nolint:gocritic // must be an owner
clitest.SetupConfig(t, ownerClient, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
//nolint:gocritic // must be an owner
members, err := ownerClient.OrganizationMembers(ctx, otherOrg.ID)
require.NoError(t, err)
require.Len(t, members, 2)
})
}
func TestDeleteOrganizationsByUser(t *testing.T) {
t.Parallel()
t.Run("Default", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, user := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
// nolint:gocritic // owner used below to delete
o, err := client.Organization(ctx, user.OrganizationID)
require.NoError(t, err)
// nolint:gocritic // only owners can delete orgs
err = client.DeleteOrganization(ctx, o.ID.String())
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("DeleteById", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
// nolint:gocritic // only owners can delete orgs
err := client.DeleteOrganization(ctx, o.ID.String())
require.NoError(t, err)
})
t.Run("DeleteByName", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
// nolint:gocritic // only owners can delete orgs
err := client.DeleteOrganization(ctx, o.Name)
require.NoError(t, err)
})
}
func TestPatchOrganizationsByUser(t *testing.T) {
t.Parallel()
t.Run("Conflict", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, user := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
// nolint:gocritic // owner used below as only they can create orgs
originalOrg, err := client.Organization(ctx, user.OrganizationID)
require.NoError(t, err)
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
// nolint:gocritic // owner used above to make the org
_, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{
Name: originalOrg.Name,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("ReservedName", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
var err error
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
_, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{
Name: codersdk.DefaultOrganization,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("InvalidName", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
var err error
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
_, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{
Name: "something unique but not url safe",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("UpdateById", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
var err error
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
o, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{
Name: "new-new-org",
})
require.NoError(t, err)
require.Equal(t, "new-new-org", o.Name)
})
t.Run("UpdateByName", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
const displayName = "New Organization"
var err error
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) {
request.DisplayName = displayName
})
o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{
Name: "new-new-org",
})
require.NoError(t, err)
require.Equal(t, "new-new-org", o.Name)
require.Equal(t, displayName, o.DisplayName) // didn't change
})
t.Run("UpdateDisplayName", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
var err error
const name = "new-org"
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) {
request.Name = name
})
const displayName = "The Newest One"
o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{
DisplayName: "The Newest One",
})
require.NoError(t, err)
require.Equal(t, "new-org", o.Name) // didn't change
require.Equal(t, displayName, o.DisplayName)
})
t.Run("UpdateDescription", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
const displayName = "New Organization"
var err error
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) {
request.DisplayName = displayName
request.Name = "new-org"
})
const description = "wow, this organization description is so updated!"
o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{
Description: ptr.Ref(description),
})
require.NoError(t, err)
require.Equal(t, "new-org", o.Name) // didn't change
require.Equal(t, displayName, o.DisplayName) // didn't change
require.Equal(t, description, o.Description)
})
t.Run("UpdateIcon", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
const displayName = "New Organization"
var err error
o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) {
request.DisplayName = displayName
request.Icon = "/emojis/random.png"
request.Name = "new-org"
})
const icon = "/emojis/1f48f-1f3ff.png"
o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{
Icon: ptr.Ref(icon),
})
require.NoError(t, err)
require.Equal(t, "new-org", o.Name) // didn't change
require.Equal(t, displayName, o.DisplayName) // didn't change
require.Equal(t, icon, o.Icon)
})
}
func TestPostOrganizationsByUser(t *testing.T) {
t.Parallel()
t.Run("Conflict", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, user := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // using owner for below
org, err := client.Organization(ctx, user.OrganizationID)
require.NoError(t, err)
//nolint:gocritic // only owners can create orgs
_, err = client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: org.Name,
DisplayName: org.DisplayName,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("InvalidName", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // only owners can create orgs
_, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "A name which is definitely not url safe",
DisplayName: "New",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // only owners can create orgs
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "new-org",
DisplayName: "New organization",
Description: "A new organization to love and cherish forever.",
Icon: "/emojis/1f48f-1f3ff.png",
})
require.NoError(t, err)
require.Equal(t, "new-org", o.Name)
require.Equal(t, "New organization", o.DisplayName)
require.Equal(t, "A new organization to love and cherish forever.", o.Description)
require.Equal(t, "/emojis/1f48f-1f3ff.png", o.Icon)
})
t.Run("CreateWithoutExplicitDisplayName", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // only owners can create orgs
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "new-org",
})
require.NoError(t, err)
require.Equal(t, "new-org", o.Name)
require.Equal(t, "new-org", o.DisplayName) // should match the given `Name`
})
}