Files
substrate/cmd/ateapi/internal/controlapi/functionaltest/atespace_test.go
T
Julian Gutierrez Oschmann 985c200247 Add DeletePreconditions to all Delete<Resource> methods (#1807)
Allow clients to do optimistic locking on deletes, as well as prevent
unintended deletes on name reuse.

#866
2026-09-23 18:55:59 +00:00

294 lines
12 KiB
Go

// Copyright 2026 Google LLC
//
// 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 functionaltest
import (
"context"
"testing"
"time"
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestCreateAtespace_Success(t *testing.T) {
ns := namespaceForTest("ns-create-atespace")
tc := setupTest(t, ns)
defer tc.cleanup()
createTemplate(t, tc, ns)
resp, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{
Atespace: &ateapipb.Atespace{
Metadata: &ateapipb.ResourceMetadata{
Name: "team-a",
Uid: "caller-supplied-uid",
Version: 999,
CreateTime: timestamppb.New(time.Unix(1, 0)),
UpdateTime: timestamppb.New(time.Unix(1, 0)),
},
},
})
if err != nil {
t.Fatalf("CreateAtespace failed: %v", err)
}
md := resp.GetMetadata()
if md.GetName() != "team-a" {
t.Errorf("Name = %q, want team-a", md.GetName())
}
if md.GetAtespace() != "" {
t.Errorf("Atespace = %q, want empty (global-scoped)", md.GetAtespace())
}
if md.GetVersion() != 1 {
t.Errorf("Version = %d, want 1 (caller-set 999 must be ignored)", md.GetVersion())
}
if md.GetUid() == "" || md.GetUid() == "caller-supplied-uid" {
t.Errorf("uid = %q, want a server-generated value", md.GetUid())
}
if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{
Actor: &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{
Atespace: "team-a",
Name: "id1"},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
}}); err != nil {
t.Errorf("CreateActor into freshly created atespace failed: %v", err)
}
}
func TestCreateAtespace_AlreadyExists(t *testing.T) {
ns := namespaceForTest("ns-create-atespace-dup")
tc := setupTest(t, ns)
defer tc.cleanup()
if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: "team-a"}}}); err != nil {
t.Fatalf("first CreateAtespace failed: %v", err)
}
_, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: "team-a"}}})
assertGrpcError(t, err, codes.AlreadyExists, "Atespace team-a already exists")
}
func TestGetAtespace_Found(t *testing.T) {
ns := namespaceForTest("ns-get-atespace")
tc := setupTest(t, ns)
defer tc.cleanup()
created, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: "team-a"}}})
if err != nil {
t.Fatalf("CreateAtespace failed: %v", err)
}
resp, err := tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}})
if err != nil {
t.Fatalf("GetAtespace failed: %v", err)
}
if diff := cmp.Diff(created, resp, protocmp.Transform()); diff != "" {
t.Errorf("GetAtespace mismatch (-created +got):\n%s", diff)
}
}
func TestGetAtespace_NotFound(t *testing.T) {
ns := namespaceForTest("ns-get-atespace-missing")
tc := setupTest(t, ns)
defer tc.cleanup()
_, err := tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "nope"}})
assertGrpcError(t, err, codes.NotFound, "Atespace nope not found")
}
func TestListAtespaces(t *testing.T) {
ns := namespaceForTest("ns-list-atespaces")
tc := setupTest(t, ns)
defer tc.cleanup()
for _, n := range []string{"team-a", "team-b"} {
if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: n}}}); err != nil {
t.Fatalf("CreateAtespace(%s) failed: %v", n, err)
}
}
resp, err := tc.client.ListAtespaces(context.Background(), &ateapipb.ListAtespacesRequest{})
if err != nil {
t.Fatalf("ListAtespaces failed: %v", err)
}
got := map[string]bool{}
for _, a := range resp.GetAtespaces() {
got[a.GetMetadata().GetName()] = true
}
// setupTest seeds testAtespace; team-a and team-b were created above.
for _, n := range []string{testAtespace, "team-a", "team-b"} {
if !got[n] {
t.Errorf("ListAtespaces missing %q; got %v", n, got)
}
}
}
func TestDeleteAtespace_Empty_Success(t *testing.T) {
ns := namespaceForTest("ns-delete-atespace-empty")
tc := setupTest(t, ns)
defer tc.cleanup()
if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: "team-a"}}}); err != nil {
t.Fatalf("CreateAtespace failed: %v", err)
}
deleted, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}})
if err != nil {
t.Fatalf("DeleteAtespace failed: %v", err)
}
// DeleteAtespace returns the deleted resource.
if got := deleted.GetMetadata().GetName(); got != "team-a" {
t.Errorf("deleted atespace name = %q, want team-a", got)
}
_, err = tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}})
assertGrpcError(t, err, codes.NotFound, "Atespace team-a not found")
}
func TestDeleteAtespace_NonEmpty_Rejected(t *testing.T) {
ns := namespaceForTest("ns-delete-atespace-nonempty")
tc := setupTest(t, ns)
defer tc.cleanup()
createTemplate(t, tc, ns)
if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: "team-a"}}}); err != nil {
t.Fatalf("CreateAtespace failed: %v", err)
}
if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "id1"},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
}}); err != nil {
t.Fatalf("CreateActor failed: %v", err)
}
_, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}})
assertGrpcError(t, err, codes.FailedPrecondition, "Atespace team-a is not empty")
// The atespace must survive a rejected delete.
if _, err := tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}}); err != nil {
t.Errorf("atespace should survive a rejected delete, got %v", err)
}
}
// TestDeleteAtespace_ScopedToTargetAtespace pins (at the RPC layer) that the
// emptiness check is scoped to the target atespace: deleting an empty atespace
// succeeds even when a different atespace holds actors.
func TestDeleteAtespace_ScopedToTargetAtespace(t *testing.T) {
ns := namespaceForTest("ns-delete-atespace-scoped")
tc := setupTest(t, ns)
defer tc.cleanup()
createTemplate(t, tc, ns)
createAtespace(t, tc, "team-a")
createAtespace(t, tc, "team-b")
// Actor only in team-b.
if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Atespace: "team-b", Name: "id1"},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
}}); err != nil {
t.Fatalf("CreateActor failed: %v", err)
}
// Empty team-a deletes fine despite team-b holding an actor.
if _, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}}); err != nil {
t.Errorf("DeleteAtespace(team-a, empty) failed: %v", err)
}
// team-b is still non-empty → rejected.
_, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "team-b"}})
assertGrpcError(t, err, codes.FailedPrecondition, "Atespace team-b is not empty")
}
func TestDeleteAtespace_NotFound(t *testing.T) {
ns := namespaceForTest("ns-delete-atespace-missing")
tc := setupTest(t, ns)
defer tc.cleanup()
_, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: "nope"}})
assertGrpcError(t, err, codes.NotFound, "Atespace nope not found")
}
func TestValidation_Atespace(t *testing.T) {
ns := namespaceForTest("ns-validation-atespace")
tc := setupTest(t, ns)
defer tc.cleanup()
t.Run("ListAtespaces", func(t *testing.T) {
_, err := tc.client.ListAtespaces(context.Background(), &ateapipb.ListAtespacesRequest{PageSize: -1})
assertGrpcErrorRegex(t, err, codes.InvalidArgument, "page_size: Invalid value")
})
t.Run("ListAtespaces invalid token", func(t *testing.T) {
_, err := tc.client.ListAtespaces(context.Background(), &ateapipb.ListAtespacesRequest{PageToken: "%%%"})
assertGrpcError(t, err, codes.InvalidArgument, "invalid page_token")
})
t.Run("CreateAtespace", func(t *testing.T) {
_, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{})
assertGrpcErrorRegex(t, err, codes.InvalidArgument, "atespace: Required value")
})
t.Run("GetAtespace", func(t *testing.T) {
_, err := tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{})
assertGrpcErrorRegex(t, err, codes.InvalidArgument, "atespace: Required value")
})
t.Run("DeleteAtespace", func(t *testing.T) {
_, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{})
assertGrpcErrorRegex(t, err, codes.InvalidArgument, "atespace: Required value")
})
}
func TestDeleteAtespace_Preconditions(t *testing.T) {
ns := namespaceForTest("ns-delete-atespace-preconditions")
tc := setupTest(t, ns)
defer tc.cleanup()
ctx := context.Background()
ref := &ateapipb.ObjectRef{Name: "team-a"}
create := func() *ateapipb.Atespace {
created, err := tc.client.CreateAtespace(ctx, &ateapipb.CreateAtespaceRequest{Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: ref.GetName()}}})
if err != nil {
t.Fatalf("CreateAtespace failed: %v", err)
}
return created
}
del := func(opts *ateapipb.DeleteOptions) error {
_, err := tc.client.DeleteAtespace(ctx, &ateapipb.DeleteAtespaceRequest{Atespace: ref, Options: opts})
return err
}
atespace := create()
uid, version := atespace.GetMetadata().GetUid(), atespace.GetMetadata().GetVersion()
assertGrpcError(t, del(&ateapipb.DeleteOptions{Version: version + 1}), codes.Aborted, "concurrent update conflict, please retry")
assertGrpcError(t, del(&ateapipb.DeleteOptions{Uid: uid, Version: version + 1}), codes.Aborted, "concurrent update conflict, please retry")
assertGrpcError(t, del(&ateapipb.DeleteOptions{Uid: foreignUID}), codes.Aborted, "Atespace team-a does not have uid "+foreignUID)
assertGrpcError(t, del(&ateapipb.DeleteOptions{Uid: foreignUID, Version: version}), codes.Aborted, "Atespace team-a does not have uid "+foreignUID)
if _, err := tc.client.GetAtespace(ctx, &ateapipb.GetAtespaceRequest{Atespace: ref}); err != nil {
t.Fatalf("a refused delete removed the atespace: %v", err)
}
if err := del(&ateapipb.DeleteOptions{Version: version}); err != nil {
t.Fatalf("DeleteAtespace with the matching version: %v", err)
}
atespace = create()
if err := del(&ateapipb.DeleteOptions{Uid: atespace.GetMetadata().GetUid()}); err != nil {
t.Fatalf("DeleteAtespace with the matching uid: %v", err)
}
atespace = create()
if err := del(&ateapipb.DeleteOptions{Uid: atespace.GetMetadata().GetUid(), Version: atespace.GetMetadata().GetVersion()}); err != nil {
t.Fatalf("DeleteAtespace with both guards: %v", err)
}
_, err := tc.client.GetAtespace(ctx, &ateapipb.GetAtespaceRequest{Atespace: ref})
assertGrpcError(t, err, codes.NotFound, "Atespace team-a not found")
}