@@ -38,9 +38,10 @@ func newCreateSpritzTestServer(t *testing.T) *server {
3838 t .Helper ()
3939 scheme := newTestSpritzScheme (t )
4040 return & server {
41- client : fake .NewClientBuilder ().WithScheme (scheme ).Build (),
42- scheme : scheme ,
43- namespace : "spritz-test" ,
41+ client : fake .NewClientBuilder ().WithScheme (scheme ).Build (),
42+ scheme : scheme ,
43+ namespace : "spritz-test" ,
44+ controlNamespace : "spritz-test" ,
4445 auth : authConfig {
4546 mode : authModeHeader ,
4647 headerID : "X-Spritz-User-Id" ,
@@ -758,6 +759,78 @@ func TestCreateSpritzAllowsProvisionerCurrentNamespaceWithoutOverride(t *testing
758759 }
759760}
760761
762+ func TestCreateSpritzRejectsExplicitNamespaceForProvisionerWhenOverrideDisabled (t * testing.T ) {
763+ s := newCreateSpritzTestServer (t )
764+ configureProvisionerTestServer (s )
765+ s .namespace = ""
766+ s .controlNamespace = "spritz-system"
767+ s .provisioners .allowedNamespaces = map [string ]struct {}{"team-a" : {}}
768+
769+ e := echo .New ()
770+ secured := e .Group ("" , s .authMiddleware ())
771+ secured .POST ("/api/spritzes" , s .createSpritz )
772+
773+ body := []byte (`{"presetId":"openclaw","ownerId":"user-123","idempotencyKey":"discord-ns-override","namespace":"team-a"}` )
774+ req := httptest .NewRequest (http .MethodPost , "/api/spritzes" , bytes .NewReader (body ))
775+ req .Header .Set (echo .HeaderContentType , echo .MIMEApplicationJSON )
776+ req .Header .Set ("X-Spritz-User-Id" , "zenobot" )
777+ req .Header .Set ("X-Spritz-Principal-Type" , "service" )
778+ req .Header .Set ("X-Spritz-Principal-Scopes" , "spritz.instances.create,spritz.instances.assign_owner" )
779+ rec := httptest .NewRecorder ()
780+
781+ e .ServeHTTP (rec , req )
782+
783+ if rec .Code != http .StatusBadRequest {
784+ t .Fatalf ("expected status 400, got %d: %s" , rec .Code , rec .Body .String ())
785+ }
786+ if ! strings .Contains (rec .Body .String (), "namespace override is not allowed" ) {
787+ t .Fatalf ("expected namespace override error, got %s" , rec .Body .String ())
788+ }
789+ }
790+
791+ func TestCreateSpritzRejectsProvisionerIdempotencyReuseAcrossNamespaces (t * testing.T ) {
792+ s := newCreateSpritzTestServer (t )
793+ configureProvisionerTestServer (s )
794+ s .namespace = ""
795+ s .controlNamespace = "spritz-system"
796+ s .provisioners .allowNamespaceOverride = true
797+ s .provisioners .allowedNamespaces = map [string ]struct {}{"team-a" : {}, "team-b" : {}}
798+
799+ e := echo .New ()
800+ secured := e .Group ("" , s .authMiddleware ())
801+ secured .POST ("/api/spritzes" , s .createSpritz )
802+
803+ first := []byte (`{"presetId":"openclaw","ownerId":"user-123","idempotencyKey":"discord-cross-ns","namespace":"team-a"}` )
804+ second := []byte (`{"presetId":"openclaw","ownerId":"user-123","idempotencyKey":"discord-cross-ns","namespace":"team-b"}` )
805+
806+ req1 := httptest .NewRequest (http .MethodPost , "/api/spritzes" , bytes .NewReader (first ))
807+ req1 .Header .Set (echo .HeaderContentType , echo .MIMEApplicationJSON )
808+ req1 .Header .Set ("X-Spritz-User-Id" , "zenobot" )
809+ req1 .Header .Set ("X-Spritz-Principal-Type" , "service" )
810+ req1 .Header .Set ("X-Spritz-Principal-Scopes" , "spritz.instances.create,spritz.instances.assign_owner" )
811+ rec1 := httptest .NewRecorder ()
812+ e .ServeHTTP (rec1 , req1 )
813+
814+ if rec1 .Code != http .StatusCreated {
815+ t .Fatalf ("expected first create to succeed, got %d: %s" , rec1 .Code , rec1 .Body .String ())
816+ }
817+
818+ req2 := httptest .NewRequest (http .MethodPost , "/api/spritzes" , bytes .NewReader (second ))
819+ req2 .Header .Set (echo .HeaderContentType , echo .MIMEApplicationJSON )
820+ req2 .Header .Set ("X-Spritz-User-Id" , "zenobot" )
821+ req2 .Header .Set ("X-Spritz-Principal-Type" , "service" )
822+ req2 .Header .Set ("X-Spritz-Principal-Scopes" , "spritz.instances.create,spritz.instances.assign_owner" )
823+ rec2 := httptest .NewRecorder ()
824+ e .ServeHTTP (rec2 , req2 )
825+
826+ if rec2 .Code != http .StatusConflict {
827+ t .Fatalf ("expected status 409, got %d: %s" , rec2 .Code , rec2 .Body .String ())
828+ }
829+ if ! strings .Contains (rec2 .Body .String (), "idempotencyKey already used with a different request" ) {
830+ t .Fatalf ("expected idempotency conflict, got %s" , rec2 .Body .String ())
831+ }
832+ }
833+
761834func TestCreateSpritzRetriesPendingIdempotencyReservationWithConflictingOccupant (t * testing.T ) {
762835 s := newCreateSpritzTestServer (t )
763836 configureProvisionerTestServer (s )
0 commit comments