@@ -4,12 +4,14 @@ import (
44"context"
55"database/sql"
66"encoding/json"
7+ "errors"
78"fmt"
89"sort"
910"testing"
1011"time"
1112
1213"github.com/google/uuid"
14+ "github.com/lib/pq"
1315"github.com/prometheus/client_golang/prometheus"
1416"github.com/stretchr/testify/assert"
1517"github.com/stretchr/testify/require"
@@ -4720,6 +4722,238 @@ func TestGetPresetsAtFailureLimit(t *testing.T) {
47204722})
47214723}
47224724
4725+ func TestWorkspaceAgentNameUniqueTrigger (t * testing.T ) {
4726+ t .Parallel ()
4727+
4728+ if ! dbtestutil .WillUsePostgres () {
4729+ t .Skip ("This test makes use of a database trigger not implemented in dbmem" )
4730+ }
4731+
4732+ createWorkspaceWithAgent := func (t * testing.T ,db database.Store ,org database.Organization ,agentName string ) (database.WorkspaceBuild , database.WorkspaceResource , database.WorkspaceAgent ) {
4733+ t .Helper ()
4734+
4735+ user := dbgen .User (t ,db , database.User {})
4736+ template := dbgen .Template (t ,db , database.Template {
4737+ OrganizationID :org .ID ,
4738+ CreatedBy :user .ID ,
4739+ })
4740+ templateVersion := dbgen .TemplateVersion (t ,db , database.TemplateVersion {
4741+ TemplateID : uuid.NullUUID {Valid :true ,UUID :template .ID },
4742+ OrganizationID :org .ID ,
4743+ CreatedBy :user .ID ,
4744+ })
4745+ workspace := dbgen .Workspace (t ,db , database.WorkspaceTable {
4746+ OrganizationID :org .ID ,
4747+ TemplateID :template .ID ,
4748+ OwnerID :user .ID ,
4749+ })
4750+ job := dbgen .ProvisionerJob (t ,db ,nil , database.ProvisionerJob {
4751+ Type :database .ProvisionerJobTypeWorkspaceBuild ,
4752+ OrganizationID :org .ID ,
4753+ })
4754+ build := dbgen .WorkspaceBuild (t ,db , database.WorkspaceBuild {
4755+ BuildNumber :1 ,
4756+ JobID :job .ID ,
4757+ WorkspaceID :workspace .ID ,
4758+ TemplateVersionID :templateVersion .ID ,
4759+ })
4760+ resource := dbgen .WorkspaceResource (t ,db , database.WorkspaceResource {
4761+ JobID :build .JobID ,
4762+ })
4763+ agent := dbgen .WorkspaceAgent (t ,db , database.WorkspaceAgent {
4764+ ResourceID :resource .ID ,
4765+ Name :agentName ,
4766+ })
4767+
4768+ return build ,resource ,agent
4769+ }
4770+
4771+ t .Run ("DuplicateNamesInSameWorkspaceResource" ,func (t * testing.T ) {
4772+ t .Parallel ()
4773+
4774+ db ,_ := dbtestutil .NewDB (t )
4775+ org := dbgen .Organization (t ,db , database.Organization {})
4776+ ctx := testutil .Context (t ,testutil .WaitShort )
4777+
4778+ // Given: A workspace with an agent
4779+ _ ,resource ,_ := createWorkspaceWithAgent (t ,db ,org ,"duplicate-agent" )
4780+
4781+ // When: Another agent is created for that workspace with the same name.
4782+ _ ,err := db .InsertWorkspaceAgent (ctx , database.InsertWorkspaceAgentParams {
4783+ ID :uuid .New (),
4784+ CreatedAt :time .Now (),
4785+ UpdatedAt :time .Now (),
4786+ Name :"duplicate-agent" ,// Same name as agent1
4787+ ResourceID :resource .ID ,
4788+ AuthToken :uuid .New (),
4789+ Architecture :"amd64" ,
4790+ OperatingSystem :"linux" ,
4791+ APIKeyScope :database .AgentKeyScopeEnumAll ,
4792+ })
4793+
4794+ // Then: We expect it to fail.
4795+ require .Error (t ,err )
4796+ var pqErr * pq.Error
4797+ require .True (t ,errors .As (err ,& pqErr ))
4798+ require .Equal (t ,pq .ErrorCode ("23505" ),pqErr .Code )// unique_violation
4799+ require .Contains (t ,pqErr .Message ,`workspace agent name "duplicate-agent" already exists in this workspace build` )
4800+ })
4801+
4802+ t .Run ("DuplicateNamesInSameProvisionerJob" ,func (t * testing.T ) {
4803+ t .Parallel ()
4804+
4805+ db ,_ := dbtestutil .NewDB (t )
4806+ org := dbgen .Organization (t ,db , database.Organization {})
4807+ ctx := testutil .Context (t ,testutil .WaitShort )
4808+
4809+ // Given: A workspace with an agent
4810+ _ ,resource ,agent := createWorkspaceWithAgent (t ,db ,org ,"duplicate-agent" )
4811+
4812+ // When: A child agent is created for that workspace with the same name.
4813+ _ ,err := db .InsertWorkspaceAgent (ctx , database.InsertWorkspaceAgentParams {
4814+ ID :uuid .New (),
4815+ CreatedAt :time .Now (),
4816+ UpdatedAt :time .Now (),
4817+ Name :agent .Name ,
4818+ ResourceID :resource .ID ,
4819+ AuthToken :uuid .New (),
4820+ Architecture :"amd64" ,
4821+ OperatingSystem :"linux" ,
4822+ APIKeyScope :database .AgentKeyScopeEnumAll ,
4823+ })
4824+
4825+ // Then: We expect it to fail.
4826+ require .Error (t ,err )
4827+ var pqErr * pq.Error
4828+ require .True (t ,errors .As (err ,& pqErr ))
4829+ require .Equal (t ,pq .ErrorCode ("23505" ),pqErr .Code )// unique_violation
4830+ require .Contains (t ,pqErr .Message ,`workspace agent name "duplicate-agent" already exists in this workspace build` )
4831+ })
4832+
4833+ t .Run ("DuplicateChildNamesOverMultipleResources" ,func (t * testing.T ) {
4834+ t .Parallel ()
4835+
4836+ db ,_ := dbtestutil .NewDB (t )
4837+ org := dbgen .Organization (t ,db , database.Organization {})
4838+ ctx := testutil .Context (t ,testutil .WaitShort )
4839+
4840+ // Given: A workspace with two agents
4841+ _ ,resource1 ,agent1 := createWorkspaceWithAgent (t ,db ,org ,"parent-agent-1" )
4842+
4843+ resource2 := dbgen .WorkspaceResource (t ,db , database.WorkspaceResource {JobID :resource1 .JobID })
4844+ agent2 := dbgen .WorkspaceAgent (t ,db , database.WorkspaceAgent {
4845+ ResourceID :resource2 .ID ,
4846+ Name :"parent-agent-2" ,
4847+ })
4848+
4849+ // Given: One agent has a child agent
4850+ agent1Child := dbgen .WorkspaceAgent (t ,db , database.WorkspaceAgent {
4851+ ParentID : uuid.NullUUID {Valid :true ,UUID :agent1 .ID },
4852+ Name :"child-agent" ,
4853+ ResourceID :resource1 .ID ,
4854+ })
4855+
4856+ // When: A child agent is inserted for the other parent.
4857+ _ ,err := db .InsertWorkspaceAgent (ctx , database.InsertWorkspaceAgentParams {
4858+ ID :uuid .New (),
4859+ ParentID : uuid.NullUUID {Valid :true ,UUID :agent2 .ID },
4860+ CreatedAt :time .Now (),
4861+ UpdatedAt :time .Now (),
4862+ Name :agent1Child .Name ,
4863+ ResourceID :resource2 .ID ,
4864+ AuthToken :uuid .New (),
4865+ Architecture :"amd64" ,
4866+ OperatingSystem :"linux" ,
4867+ APIKeyScope :database .AgentKeyScopeEnumAll ,
4868+ })
4869+
4870+ // Then: We expect it to fail.
4871+ require .Error (t ,err )
4872+ var pqErr * pq.Error
4873+ require .True (t ,errors .As (err ,& pqErr ))
4874+ require .Equal (t ,pq .ErrorCode ("23505" ),pqErr .Code )// unique_violation
4875+ require .Contains (t ,pqErr .Message ,`workspace agent name "child-agent" already exists in this workspace build` )
4876+ })
4877+
4878+ t .Run ("SameNamesInDifferentWorkspaces" ,func (t * testing.T ) {
4879+ t .Parallel ()
4880+
4881+ agentName := "same-name-different-workspace"
4882+
4883+ db ,_ := dbtestutil .NewDB (t )
4884+ org := dbgen .Organization (t ,db , database.Organization {})
4885+
4886+ // Given: A workspace with an agent
4887+ _ ,_ ,agent1 := createWorkspaceWithAgent (t ,db ,org ,agentName )
4888+ require .Equal (t ,agentName ,agent1 .Name )
4889+
4890+ // When: A second workspace is created with an agent having the same name
4891+ _ ,_ ,agent2 := createWorkspaceWithAgent (t ,db ,org ,agentName )
4892+ require .Equal (t ,agentName ,agent2 .Name )
4893+
4894+ // Then: We expect there to be different agents with the same name.
4895+ require .NotEqual (t ,agent1 .ID ,agent2 .ID )
4896+ require .Equal (t ,agent1 .Name ,agent2 .Name )
4897+ })
4898+
4899+ t .Run ("NullWorkspaceID" ,func (t * testing.T ) {
4900+ t .Parallel ()
4901+
4902+ db ,_ := dbtestutil .NewDB (t )
4903+ org := dbgen .Organization (t ,db , database.Organization {})
4904+ ctx := testutil .Context (t ,testutil .WaitShort )
4905+
4906+ // Given: A resource that does not belong to a workspace build (simulating template import)
4907+ orphanJob := dbgen .ProvisionerJob (t ,db ,nil , database.ProvisionerJob {
4908+ Type :database .ProvisionerJobTypeTemplateVersionImport ,
4909+ OrganizationID :org .ID ,
4910+ })
4911+ orphanResource := dbgen .WorkspaceResource (t ,db , database.WorkspaceResource {
4912+ JobID :orphanJob .ID ,
4913+ })
4914+
4915+ // And this resource has a workspace agent.
4916+ agent1 ,err := db .InsertWorkspaceAgent (ctx , database.InsertWorkspaceAgentParams {
4917+ ID :uuid .New (),
4918+ CreatedAt :time .Now (),
4919+ UpdatedAt :time .Now (),
4920+ Name :"orphan-agent" ,
4921+ ResourceID :orphanResource .ID ,
4922+ AuthToken :uuid .New (),
4923+ Architecture :"amd64" ,
4924+ OperatingSystem :"linux" ,
4925+ APIKeyScope :database .AgentKeyScopeEnumAll ,
4926+ })
4927+ require .NoError (t ,err )
4928+ require .Equal (t ,"orphan-agent" ,agent1 .Name )
4929+
4930+ // When: We created another resource that does not belong to a workspace build.
4931+ orphanJob2 := dbgen .ProvisionerJob (t ,db ,nil , database.ProvisionerJob {
4932+ Type :database .ProvisionerJobTypeTemplateVersionImport ,
4933+ OrganizationID :org .ID ,
4934+ })
4935+ orphanResource2 := dbgen .WorkspaceResource (t ,db , database.WorkspaceResource {
4936+ JobID :orphanJob2 .ID ,
4937+ })
4938+
4939+ // Then: We expect to be able to create an agent in this new resource that has the same name.
4940+ agent2 ,err := db .InsertWorkspaceAgent (ctx , database.InsertWorkspaceAgentParams {
4941+ ID :uuid .New (),
4942+ CreatedAt :time .Now (),
4943+ UpdatedAt :time .Now (),
4944+ Name :"orphan-agent" ,// Same name as agent1
4945+ ResourceID :orphanResource2 .ID ,
4946+ AuthToken :uuid .New (),
4947+ Architecture :"amd64" ,
4948+ OperatingSystem :"linux" ,
4949+ APIKeyScope :database .AgentKeyScopeEnumAll ,
4950+ })
4951+ require .NoError (t ,err )
4952+ require .Equal (t ,"orphan-agent" ,agent2 .Name )
4953+ require .NotEqual (t ,agent1 .ID ,agent2 .ID )
4954+ })
4955+ }
4956+
47234957func requireUsersMatch (t testing.TB ,expected []database.User ,found []database.GetUsersRow ,msg string ) {
47244958t .Helper ()
47254959require .ElementsMatch (t ,expected ,database .ConvertUserRows (found ),msg )