diff --git a/service/integration/attributes_test.go b/service/integration/attributes_test.go index c3e67b68f9..2e2c29c32b 100644 --- a/service/integration/attributes_test.go +++ b/service/integration/attributes_test.go @@ -510,7 +510,8 @@ func (s *AttributesSuite) Test_ListAttributes_OrdersByCreatedAt_Succeeds() { func (s *AttributesSuite) Test_ListAttributes_SortByName_ASC() { nsID := s.createSortTestNamespace("sort-name-asc") - ids := s.createNamedSortTestAttributes(nsID, []string{"aaa-sort", "bbb-sort", "ccc-sort"}) + ids := s.createSortTestAttributes(nsID, []string{"aaa-sort", "bbb-sort", "ccc-sort"}) + defer s.deleteSortTestAttributes(ids) listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ Namespace: nsID, @@ -527,7 +528,8 @@ func (s *AttributesSuite) Test_ListAttributes_SortByName_ASC() { func (s *AttributesSuite) Test_ListAttributes_SortByName_DESC() { nsID := s.createSortTestNamespace("sort-name-desc") - ids := s.createNamedSortTestAttributes(nsID, []string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) + ids := s.createSortTestAttributes(nsID, []string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) + defer s.deleteSortTestAttributes(ids) listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ Namespace: nsID, @@ -544,7 +546,8 @@ func (s *AttributesSuite) Test_ListAttributes_SortByName_DESC() { func (s *AttributesSuite) Test_ListAttributes_SortByCreatedAt_ASC() { nsID := s.createSortTestNamespace("sort-created-asc") - ids := s.createSortTestAttributes(nsID, "createdasc-attr", 3) + ids := s.createSortTestAttributes(nsID, []string{"createdasc-attr-0", "createdasc-attr-1", "createdasc-attr-2"}) + defer s.deleteSortTestAttributes(ids) listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ Namespace: nsID, @@ -561,7 +564,8 @@ func (s *AttributesSuite) Test_ListAttributes_SortByCreatedAt_ASC() { func (s *AttributesSuite) Test_ListAttributes_SortByCreatedAt_DESC() { nsID := s.createSortTestNamespace("sort-created-desc") - ids := s.createSortTestAttributes(nsID, "createddesc-attr", 3) + ids := s.createSortTestAttributes(nsID, []string{"createddesc-attr-0", "createddesc-attr-1", "createddesc-attr-2"}) + defer s.deleteSortTestAttributes(ids) listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ Namespace: nsID, @@ -578,7 +582,8 @@ func (s *AttributesSuite) Test_ListAttributes_SortByCreatedAt_DESC() { func (s *AttributesSuite) Test_ListAttributes_SortByUpdatedAt_DESC() { nsID := s.createSortTestNamespace("sort-updated-desc") - ids := s.createSortTestAttributes(nsID, "upd-sort-attr", 3) + ids := s.createSortTestAttributes(nsID, []string{"upd-sort-attr-0", "upd-sort-attr-1", "upd-sort-attr-2"}) + defer s.deleteSortTestAttributes(ids) // Update the first attribute so its updated_at is the most recent time.Sleep(5 * time.Millisecond) @@ -606,7 +611,8 @@ func (s *AttributesSuite) Test_ListAttributes_SortByUpdatedAt_DESC() { func (s *AttributesSuite) Test_ListAttributes_SortByUpdatedAt_ASC() { nsID := s.createSortTestNamespace("sort-updated-asc") - ids := s.createSortTestAttributes(nsID, "upd-sort-asc-attr", 3) + ids := s.createSortTestAttributes(nsID, []string{"upd-sort-asc-attr-0", "upd-sort-asc-attr-1", "upd-sort-asc-attr-2"}) + defer s.deleteSortTestAttributes(ids) // Update the last attribute so its updated_at is the most recent time.Sleep(5 * time.Millisecond) @@ -632,9 +638,42 @@ func (s *AttributesSuite) Test_ListAttributes_SortByUpdatedAt_ASC() { assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, ids[0], ids[1], ids[2]) } +func (s *AttributesSuite) Test_ListAttributes_SortTieBreaker_CreatedAtWithIDFallback() { + nsID := s.createSortTestNamespace("sort-tiebreaker") + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-attr-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: name, + NamespaceId: nsID, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestAttributes(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "attribute_definitions", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ + Namespace: nsID, + Sort: []*attributes.AttributesSort{ + {Field: attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetAttributes(), func(attr *policy.Attribute) string { return attr.GetId() }, sorted[0], sorted[1], sorted[2]) +} + func (s *AttributesSuite) Test_ListAttributes_SortByUnspecifiedField_DefaultsToCreatedAt() { nsID := s.createSortTestNamespace("sort-unspecified-field") - ids := s.createSortTestAttributes(nsID, "unspecified-field-attr", 3) + ids := s.createSortTestAttributes(nsID, []string{"unspecified-field-attr-0", "unspecified-field-attr-1", "unspecified-field-attr-2"}) + defer s.deleteSortTestAttributes(ids) listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ Namespace: nsID, @@ -651,7 +690,8 @@ func (s *AttributesSuite) Test_ListAttributes_SortByUnspecifiedField_DefaultsToC func (s *AttributesSuite) Test_ListAttributes_SortByUnspecifiedDirection_DefaultsToDESC() { nsID := s.createSortTestNamespace("sort-unspecified-dir") - ids := s.createSortTestAttributes(nsID, "unspecified-dir-attr", 3) + ids := s.createSortTestAttributes(nsID, []string{"unspecified-dir-attr-0", "unspecified-dir-attr-1", "unspecified-dir-attr-2"}) + defer s.deleteSortTestAttributes(ids) listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ Namespace: nsID, @@ -668,7 +708,8 @@ func (s *AttributesSuite) Test_ListAttributes_SortByUnspecifiedDirection_Default func (s *AttributesSuite) Test_ListAttributes_SortByBothUnspecified_DefaultsToCreatedAtDESC() { nsID := s.createSortTestNamespace("sort-both-unspecified") - ids := s.createSortTestAttributes(nsID, "both-unspecified-attr", 3) + ids := s.createSortTestAttributes(nsID, []string{"both-unspecified-attr-0", "both-unspecified-attr-1", "both-unspecified-attr-2"}) + defer s.deleteSortTestAttributes(ids) listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ Namespace: nsID, @@ -685,7 +726,8 @@ func (s *AttributesSuite) Test_ListAttributes_SortByBothUnspecified_DefaultsToCr func (s *AttributesSuite) Test_ListAttributes_SortOmitted() { nsID := s.createSortTestNamespace("sort-omitted") - ids := s.createSortTestAttributes(nsID, "omitted-sort-attr", 3) + ids := s.createSortTestAttributes(nsID, []string{"omitted-sort-attr-0", "omitted-sort-attr-1", "omitted-sort-attr-2"}) + defer s.deleteSortTestAttributes(ids) listRsp, err := s.db.PolicyClient.ListAttributes(s.ctx, &attributes.ListAttributesRequest{ Namespace: nsID, @@ -1849,15 +1891,15 @@ func (s *AttributesSuite) createSortTestNamespace(label string) string { return ns.GetId() } -// createSortTestAttributes creates count attributes in the given namespace with 5ms gaps for distinct timestamps. -// Returns the attribute IDs in creation order. -func (s *AttributesSuite) createSortTestAttributes(nsID string, label string, count int) []string { //nolint:unparam // count is parameterized for reuse across endpoint test files - ids := make([]string, count) - for i := range count { +// createSortTestAttributes creates attributes in the given namespace with the given prefixes, +// adding 5ms gaps between creations for distinct timestamps. Returns the attribute IDs in creation order. +func (s *AttributesSuite) createSortTestAttributes(nsID string, prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { if i > 0 { time.Sleep(5 * time.Millisecond) } - name := fmt.Sprintf("%s-%d-%d", label, i, time.Now().UnixNano()) + name := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) created, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ Name: name, NamespaceId: nsID, @@ -1869,22 +1911,9 @@ func (s *AttributesSuite) createSortTestAttributes(nsID string, label string, co return ids } -// createNamedSortTestAttributes creates attributes with specific name prefixes for name-sort testing. -// Returns the attribute IDs in the same order as the prefixes. -func (s *AttributesSuite) createNamedSortTestAttributes(nsID string, prefixes []string) []string { - suffix := time.Now().UnixNano() - ids := make([]string, len(prefixes)) - for i, prefix := range prefixes { - name := fmt.Sprintf("%s-%d", prefix, suffix) - created, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ - Name: name, - NamespaceId: nsID, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, - }) - s.Require().NoError(err) - ids[i] = created.GetId() - } - return ids +// deleteSortTestAttributes deactivates attributes created by sort tests. +func (s *AttributesSuite) deleteSortTestAttributes(ids []string) { + s.Require().NoError(forceDeleteRows(s.ctx, s.db, "attribute_definitions", ids)) } func (s *AttributesSuite) getAttributeFixtures() map[string]fixtures.FixtureDataAttribute { diff --git a/service/integration/kas_registry_key_test.go b/service/integration/kas_registry_key_test.go index a47eb14859..819ce9ff59 100644 --- a/service/integration/kas_registry_key_test.go +++ b/service/integration/kas_registry_key_test.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "log/slog" + "slices" "testing" "time" @@ -2474,7 +2475,7 @@ func (s *KasRegistryKeySuite) createKeyAndKas() *policy.KasKey { } func (s *KasRegistryKeySuite) Test_ListKeys_SortByKeyId_ASC() { - ids, kasID := s.createKeyIDSortTestKasKeys([]string{"aaa-kksort", "bbb-kksort", "ccc-kksort"}) + ids, kasID := s.createSortTestKasKeys([]string{"aaa-kksort", "bbb-kksort", "ccc-kksort"}) defer s.deleteSortTestKasKeys(ids, kasID) list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ @@ -2491,7 +2492,7 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByKeyId_ASC() { } func (s *KasRegistryKeySuite) Test_ListKeys_SortByKeyId_DESC() { - ids, kasID := s.createKeyIDSortTestKasKeys([]string{"aaa-kksortdesc", "bbb-kksortdesc", "ccc-kksortdesc"}) + ids, kasID := s.createSortTestKasKeys([]string{"aaa-kksortdesc", "bbb-kksortdesc", "ccc-kksortdesc"}) defer s.deleteSortTestKasKeys(ids, kasID) list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ @@ -2508,7 +2509,7 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByKeyId_DESC() { } func (s *KasRegistryKeySuite) Test_ListKeys_SortByCreatedAt_ASC() { - ids, kasID := s.createSortTestKasKeys("createdasc-kk") + ids, kasID := s.createSortTestKasKeys([]string{"createdasc-kk-0", "createdasc-kk-1", "createdasc-kk-2"}) defer s.deleteSortTestKasKeys(ids, kasID) list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ @@ -2525,7 +2526,7 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByCreatedAt_ASC() { } func (s *KasRegistryKeySuite) Test_ListKeys_SortByCreatedAt_DESC() { - ids, kasID := s.createSortTestKasKeys("createddesc-kk") + ids, kasID := s.createSortTestKasKeys([]string{"createddesc-kk-0", "createddesc-kk-1", "createddesc-kk-2"}) defer s.deleteSortTestKasKeys(ids, kasID) list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ @@ -2542,7 +2543,7 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByCreatedAt_DESC() { } func (s *KasRegistryKeySuite) Test_ListKeys_SortByUpdatedAt_DESC() { - ids, kasID := s.createSortTestKasKeys("upd-sort-kk") + ids, kasID := s.createSortTestKasKeys([]string{"upd-sort-kk-0", "upd-sort-kk-1", "upd-sort-kk-2"}) defer s.deleteSortTestKasKeys(ids, kasID) // Update the first key so its updated_at is the most recent @@ -2570,7 +2571,7 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByUpdatedAt_DESC() { } func (s *KasRegistryKeySuite) Test_ListKeys_SortByUpdatedAt_ASC() { - ids, kasID := s.createSortTestKasKeys("updasc-kk") + ids, kasID := s.createSortTestKasKeys([]string{"updasc-kk-0", "updasc-kk-1", "updasc-kk-2"}) defer s.deleteSortTestKasKeys(ids, kasID) // Update the last key so its updated_at is the most recent @@ -2597,8 +2598,54 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByUpdatedAt_ASC() { assertIDsInOrder(s.T(), list.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, ids[0], ids[1], ids[2]) } +func (s *KasRegistryKeySuite) Test_ListKeys_SortTieBreaker_CreatedAtWithIDFallback() { + kasReq := kasregistry.CreateKeyAccessServerRequest{ + Name: "tiebreaker-kk-kas-" + uuid.NewString(), + Uri: "https://tiebreaker-kk-kas-" + uuid.NewString() + ".opentdf.io", + } + kas, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasReq) + s.Require().NoError(err) + + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + keyReq := kasregistry.CreateKeyRequest{ + KasId: kas.GetId(), + KeyId: fmt.Sprintf("tiebreaker-kk-%d-%d", i, suffix), + KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, + KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, + PrivateKeyCtx: &policy.PrivateKeyCtx{ + KeyId: fmt.Sprintf("tiebreaker-kk-priv-%d-%d", i, suffix), + WrappedKey: keyCtx, + }, + } + resp, err := s.db.PolicyClient.CreateKey(s.ctx, &keyReq) + s.Require().NoError(err) + ids[i] = resp.GetKasKey().GetKey().GetId() + } + defer s.deleteSortTestKasKeys(ids, kas.GetId()) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "key_access_server_keys", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ + KasFilter: &kasregistry.ListKeysRequest_KasId{ + KasId: kas.GetId(), + }, + Sort: []*kasregistry.KasKeysSort{ + {Field: kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKasKeys(), func(k *policy.KasKey) string { return k.GetKey().GetId() }, sorted[0], sorted[1], sorted[2]) +} + func (s *KasRegistryKeySuite) Test_ListKeys_SortByUnspecifiedField_DefaultsToCreatedAt() { - ids, kasID := s.createSortTestKasKeys("unsf-kk") + ids, kasID := s.createSortTestKasKeys([]string{"unsf-kk-0", "unsf-kk-1", "unsf-kk-2"}) defer s.deleteSortTestKasKeys(ids, kasID) list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ @@ -2615,7 +2662,7 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByUnspecifiedField_DefaultsToCre } func (s *KasRegistryKeySuite) Test_ListKeys_SortByUnspecifiedDirection_DefaultsToDESC() { - ids, kasID := s.createSortTestKasKeys("unsd-kk") + ids, kasID := s.createSortTestKasKeys([]string{"unsd-kk-0", "unsd-kk-1", "unsd-kk-2"}) defer s.deleteSortTestKasKeys(ids, kasID) list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ @@ -2632,7 +2679,7 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByUnspecifiedDirection_DefaultsT } func (s *KasRegistryKeySuite) Test_ListKeys_SortByBothUnspecified_DefaultsToCreatedAtDESC() { - ids, kasID := s.createSortTestKasKeys("unsb-kk") + ids, kasID := s.createSortTestKasKeys([]string{"unsb-kk-0", "unsb-kk-1", "unsb-kk-2"}) defer s.deleteSortTestKasKeys(ids, kasID) list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ @@ -2649,7 +2696,7 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortByBothUnspecified_DefaultsToCrea } func (s *KasRegistryKeySuite) Test_ListKeys_SortOmitted() { - ids, kasID := s.createSortTestKasKeys("omit-kk") + ids, kasID := s.createSortTestKasKeys([]string{"omit-kk-0", "omit-kk-1", "omit-kk-2"}) defer s.deleteSortTestKasKeys(ids, kasID) list, err := s.db.PolicyClient.ListKeys(s.ctx, &kasregistry.ListKeysRequest{ @@ -2664,64 +2711,36 @@ func (s *KasRegistryKeySuite) Test_ListKeys_SortOmitted() { // Sort test helpers -// createSortTestKasKeys creates 3 kas keys with 5ms gaps for distinct timestamps. -// Returns the key IDs (UUIDs) in creation order and the parent KAS ID. -func (s *KasRegistryKeySuite) createSortTestKasKeys(label string) ([]string, string) { - kasReq := kasregistry.CreateKeyAccessServerRequest{ - Name: label + "-kas-" + uuid.NewString(), - Uri: "https://" + label + "-kas-" + uuid.NewString() + ".opentdf.io", +// createSortTestKasKeys creates kas keys with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the key IDs in creation order and the parent KAS ID. +func (s *KasRegistryKeySuite) createSortTestKasKeys(prefixes []string) ([]string, string) { + label := "kas" + if len(prefixes) > 0 { + label = prefixes[0] } - kas, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasReq) - s.Require().NoError(err) - s.NotNil(kas) - - const count = 3 - ids := make([]string, count) - for i := range count { - if i > 0 { - time.Sleep(5 * time.Millisecond) - } - keyReq := kasregistry.CreateKeyRequest{ - KasId: kas.GetId(), - KeyId: fmt.Sprintf("%s-%d-%d", label, i, time.Now().UnixNano()), - KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, - KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, - PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, - PrivateKeyCtx: &policy.PrivateKeyCtx{ - KeyId: fmt.Sprintf("%s-%d", label, i), - WrappedKey: keyCtx, - }, - } - resp, err := s.db.PolicyClient.CreateKey(s.ctx, &keyReq) - s.Require().NoError(err) - s.NotNil(resp) - ids[i] = resp.GetKasKey().GetKey().GetId() - } - return ids, kas.GetId() -} - -// createKeyIDSortTestKasKeys creates kas keys with controlled key_id prefixes for lexicographic sort testing. -// Returns the key IDs (UUIDs) in the same order as the prefixes and the parent KAS ID. -func (s *KasRegistryKeySuite) createKeyIDSortTestKasKeys(prefixes []string) ([]string, string) { + kasUUID := uuid.NewString() kasReq := kasregistry.CreateKeyAccessServerRequest{ - Name: "keyidsort-kas-" + uuid.NewString(), - Uri: "https://keyidsort-kas-" + uuid.NewString() + ".opentdf.io", + Name: label + "-kas-" + kasUUID, + Uri: "https://" + label + "-kas-" + kasUUID + ".opentdf.io", } kas, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasReq) s.Require().NoError(err) s.NotNil(kas) - suffix := time.Now().UnixNano() ids := make([]string, len(prefixes)) for i, prefix := range prefixes { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + ts := time.Now().UnixNano() keyReq := kasregistry.CreateKeyRequest{ KasId: kas.GetId(), - KeyId: fmt.Sprintf("%s-%d", prefix, suffix), + KeyId: fmt.Sprintf("%s-%d", prefix, ts), KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, PrivateKeyCtx: &policy.PrivateKeyCtx{ - KeyId: fmt.Sprintf("%s-priv-%d", prefix, suffix), + KeyId: fmt.Sprintf("%s-priv-%d", prefix, ts), WrappedKey: keyCtx, }, } diff --git a/service/integration/kas_registry_test.go b/service/integration/kas_registry_test.go index ac29665409..ec35952569 100644 --- a/service/integration/kas_registry_test.go +++ b/service/integration/kas_registry_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" "strings" "testing" "time" @@ -878,7 +879,8 @@ func (s *KasRegistrySuite) Test_GetKeyAccessServer_ByIdNameUri_ReturnSameResult( } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByCreatedAt_ASC() { - ids := s.createSortTestKeyAccessServers("sort-kas-created-asc") + ids := s.createSortTestKeyAccessServers([]string{"sort-kas-created-asc-0", "sort-kas-created-asc-1", "sort-kas-created-asc-2"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -892,7 +894,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByCreatedAt_ASC() { } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByCreatedAt_DESC() { - ids := s.createSortTestKeyAccessServers("sort-kas-created-desc") + ids := s.createSortTestKeyAccessServers([]string{"sort-kas-created-desc-0", "sort-kas-created-desc-1", "sort-kas-created-desc-2"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -906,7 +909,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByCreatedAt_DESC() { } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUpdatedAt_DESC() { - ids := s.createSortTestKeyAccessServers("sort-kas-updated-desc") + ids := s.createSortTestKeyAccessServers([]string{"sort-kas-updated-desc-0", "sort-kas-updated-desc-1", "sort-kas-updated-desc-2"}) + defer s.deleteSortTestKeyAccessServers(ids) time.Sleep(5 * time.Millisecond) _, err := s.db.PolicyClient.UpdateKeyAccessServer(s.ctx, ids[0], &kasregistry.UpdateKeyAccessServerRequest{ @@ -930,7 +934,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUpdatedAt_DESC() { } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUpdatedAt_ASC() { - ids := s.createSortTestKeyAccessServers("sort-kas-updated-asc") + ids := s.createSortTestKeyAccessServers([]string{"sort-kas-updated-asc-0", "sort-kas-updated-asc-1", "sort-kas-updated-asc-2"}) + defer s.deleteSortTestKeyAccessServers(ids) time.Sleep(5 * time.Millisecond) _, err := s.db.PolicyClient.UpdateKeyAccessServer(s.ctx, ids[2], &kasregistry.UpdateKeyAccessServerRequest{ @@ -954,7 +959,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUpdatedAt_ASC() { } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByName_ASC() { - ids := s.createNamedSortTestKeyAccessServers([]string{"aaa-kas-sort", "bbb-kas-sort", "ccc-kas-sort"}) + ids := s.createSortTestKeyAccessServers([]string{"aaa-kas-sort", "bbb-kas-sort", "ccc-kas-sort"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -968,7 +974,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByName_ASC() { } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByName_DESC() { - ids := s.createNamedSortTestKeyAccessServers([]string{"aaa-kas-sortdesc", "bbb-kas-sortdesc", "ccc-kas-sortdesc"}) + ids := s.createSortTestKeyAccessServers([]string{"aaa-kas-sortdesc", "bbb-kas-sortdesc", "ccc-kas-sortdesc"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -982,7 +989,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByName_DESC() { } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUri_ASC() { - ids := s.createNamedSortTestKeyAccessServers([]string{"aaa-kas-uri", "bbb-kas-uri", "ccc-kas-uri"}) + ids := s.createSortTestKeyAccessServers([]string{"aaa-kas-uri", "bbb-kas-uri", "ccc-kas-uri"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -996,7 +1004,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUri_ASC() { } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUri_DESC() { - ids := s.createNamedSortTestKeyAccessServers([]string{"aaa-kas-uridesc", "bbb-kas-uridesc", "ccc-kas-uridesc"}) + ids := s.createSortTestKeyAccessServers([]string{"aaa-kas-uridesc", "bbb-kas-uridesc", "ccc-kas-uridesc"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -1009,8 +1018,38 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUri_DESC() { assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, ids[2], ids[1], ids[0]) } +func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortTieBreaker_CreatedAtWithIDFallback() { + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-kas-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasregistry.CreateKeyAccessServerRequest{ + Uri: fmt.Sprintf("https://%s.example.com", name), + Name: name, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestKeyAccessServers(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "key_access_servers", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ + Sort: []*kasregistry.KeyAccessServersSort{ + {Field: kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetKeyAccessServers(), func(kas *policy.KeyAccessServer) string { return kas.GetId() }, sorted[0], sorted[1], sorted[2]) +} + func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUnspecifiedField_DefaultsToCreatedAt() { - ids := s.createSortTestKeyAccessServers("unspecified-field-kas") + ids := s.createSortTestKeyAccessServers([]string{"unspecified-field-kas-0", "unspecified-field-kas-1", "unspecified-field-kas-2"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -1025,7 +1064,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUnspecifiedField_Defa } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUnspecifiedDirection_DefaultsToDESC() { - ids := s.createSortTestKeyAccessServers("unspecified-dir-kas") + ids := s.createSortTestKeyAccessServers([]string{"unspecified-dir-kas-0", "unspecified-dir-kas-1", "unspecified-dir-kas-2"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -1040,7 +1080,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByUnspecifiedDirection_ } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByBothUnspecified_DefaultsToCreatedAtDESC() { - ids := s.createSortTestKeyAccessServers("both-unspecified-kas") + ids := s.createSortTestKeyAccessServers([]string{"both-unspecified-kas-0", "both-unspecified-kas-1", "both-unspecified-kas-2"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{ Sort: []*kasregistry.KeyAccessServersSort{ @@ -1055,7 +1096,8 @@ func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortByBothUnspecified_Defau } func (s *KasRegistrySuite) Test_ListKeyAccessServers_SortOmitted() { - ids := s.createSortTestKeyAccessServers("sort-omitted-kas") + ids := s.createSortTestKeyAccessServers([]string{"sort-omitted-kas-0", "sort-omitted-kas-1", "sort-omitted-kas-2"}) + defer s.deleteSortTestKeyAccessServers(ids) listRsp, err := s.db.PolicyClient.ListKeyAccessServers(s.ctx, &kasregistry.ListKeyAccessServersRequest{}) s.Require().NoError(err) @@ -1111,16 +1153,15 @@ func (s *KasRegistrySuite) validateKasRegistryKeys(kasr *policy.KeyAccessServer) s.Len(expectedKasKeys, matchingKeysCount) } -// createSortTestKeyAccessServers creates 3 KAS entries with 5ms gaps for distinct timestamps. -// Returns the KAS IDs in creation order. -func (s *KasRegistrySuite) createSortTestKeyAccessServers(label string) []string { - const count = 3 - ids := make([]string, count) - for i := range count { +// createSortTestKeyAccessServers creates KAS entries with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the KAS IDs in creation order. +func (s *KasRegistrySuite) createSortTestKeyAccessServers(prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { if i > 0 { time.Sleep(5 * time.Millisecond) } - suffix := fmt.Sprintf("%s-%d-%d", label, i, time.Now().UnixNano()) + suffix := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) created, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasregistry.CreateKeyAccessServerRequest{ Uri: fmt.Sprintf("https://%s.example.com", suffix), Name: suffix, @@ -1131,20 +1172,12 @@ func (s *KasRegistrySuite) createSortTestKeyAccessServers(label string) []string return ids } -// createNamedSortTestKeyAccessServers creates KAS entries with specific name prefixes for deterministic name/uri sorting. -// Returns the KAS IDs in prefix order. -func (s *KasRegistrySuite) createNamedSortTestKeyAccessServers(prefixes []string) []string { - suffix := time.Now().UnixNano() - ids := make([]string, len(prefixes)) - for i, prefix := range prefixes { - created, err := s.db.PolicyClient.CreateKeyAccessServer(s.ctx, &kasregistry.CreateKeyAccessServerRequest{ - Uri: fmt.Sprintf("https://%s-%d.example.com", prefix, suffix), - Name: fmt.Sprintf("%s-%d", prefix, suffix), - }) +// deleteSortTestKeyAccessServers cleans up KAS entries created by sort tests. +func (s *KasRegistrySuite) deleteSortTestKeyAccessServers(ids []string) { + for _, id := range ids { + _, err := s.db.PolicyClient.DeleteKeyAccessServer(s.ctx, id) s.Require().NoError(err) - ids[i] = created.GetId() } - return ids } func TestKasRegistrySuite(t *testing.T) { diff --git a/service/integration/namespaces_test.go b/service/integration/namespaces_test.go index dc74b3276d..e4e9dbd985 100644 --- a/service/integration/namespaces_test.go +++ b/service/integration/namespaces_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" "strings" "testing" "time" @@ -271,7 +272,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_OrdersByCreatedAt_Succeeds() { } func (s *NamespacesSuite) Test_ListNamespaces_SortByName_ASC() { - ids := s.createNamedSortTestNamespaces([]string{"aaa-sort", "bbb-sort", "ccc-sort"}) + ids := s.createSortTestNamespaces([]string{"aaa-sort", "bbb-sort", "ccc-sort"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -287,7 +289,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByName_ASC() { } func (s *NamespacesSuite) Test_ListNamespaces_SortByName_DESC() { - ids := s.createNamedSortTestNamespaces([]string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) + ids := s.createSortTestNamespaces([]string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -303,7 +306,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByName_DESC() { } func (s *NamespacesSuite) Test_ListNamespaces_SortByCreatedAt_ASC() { - ids := s.createSortTestNamespaces("createdasc-ns") + ids := s.createSortTestNamespaces([]string{"createdasc-ns-0", "createdasc-ns-1", "createdasc-ns-2"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -319,7 +323,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByCreatedAt_ASC() { } func (s *NamespacesSuite) Test_ListNamespaces_SortByCreatedAt_DESC() { - ids := s.createSortTestNamespaces("createddesc-ns") + ids := s.createSortTestNamespaces([]string{"createddesc-ns-0", "createddesc-ns-1", "createddesc-ns-2"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -335,7 +340,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByCreatedAt_DESC() { } func (s *NamespacesSuite) Test_ListNamespaces_SortByFqn_ASC() { - ids := s.createNamedSortTestNamespaces([]string{"aaa-fqnsort", "bbb-fqnsort", "ccc-fqnsort"}) + ids := s.createSortTestNamespaces([]string{"aaa-fqnsort", "bbb-fqnsort", "ccc-fqnsort"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -351,7 +357,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByFqn_ASC() { } func (s *NamespacesSuite) Test_ListNamespaces_SortByFqn_DESC() { - ids := s.createNamedSortTestNamespaces([]string{"aaa-fqnsortdesc", "bbb-fqnsortdesc", "ccc-fqnsortdesc"}) + ids := s.createSortTestNamespaces([]string{"aaa-fqnsortdesc", "bbb-fqnsortdesc", "ccc-fqnsortdesc"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -367,7 +374,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByFqn_DESC() { } func (s *NamespacesSuite) Test_ListNamespaces_SortByUpdatedAt_DESC() { - ids := s.createSortTestNamespaces("upd-sort-ns") + ids := s.createSortTestNamespaces([]string{"upd-sort-ns-0", "upd-sort-ns-1", "upd-sort-ns-2"}) + defer s.deleteSortTestNamespaces(ids) // Update the first namespace so its updated_at is the most recent time.Sleep(5 * time.Millisecond) @@ -394,7 +402,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByUpdatedAt_DESC() { } func (s *NamespacesSuite) Test_ListNamespaces_SortByUpdatedAt_ASC() { - ids := s.createSortTestNamespaces("upd-sort-asc-ns") + ids := s.createSortTestNamespaces([]string{"upd-sort-asc-ns-0", "upd-sort-asc-ns-1", "upd-sort-asc-ns-2"}) + defer s.deleteSortTestNamespaces(ids) // Update the last namespace so its updated_at is the most recent time.Sleep(5 * time.Millisecond) @@ -420,8 +429,36 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByUpdatedAt_ASC() { assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, ids[0], ids[1], ids[2]) } +func (s *NamespacesSuite) Test_ListNamespaces_SortTieBreaker_CreatedAtWithIDFallback() { + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-ns-%d-%d.com", i, suffix) + created, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: name}) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestNamespaces(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "attribute_namespaces", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ + State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, + Sort: []*namespaces.NamespacesSort{ + {Field: namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetNamespaces(), func(ns *policy.Namespace) string { return ns.GetId() }, sorted[0], sorted[1], sorted[2]) +} + func (s *NamespacesSuite) Test_ListNamespaces_SortByUnspecifiedField_DefaultsToCreatedAt() { - ids := s.createSortTestNamespaces("unspecified-field-ns") + ids := s.createSortTestNamespaces([]string{"unspecified-sort-ns-0", "unspecified-sort-ns-1", "unspecified-sort-ns-2"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -437,7 +474,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByUnspecifiedField_DefaultsToC } func (s *NamespacesSuite) Test_ListNamespaces_SortByUnspecifiedDirection_DefaultsToDESC() { - ids := s.createSortTestNamespaces("unspecified-dir-ns") + ids := s.createSortTestNamespaces([]string{"unspecified-dir-ns-0", "unspecified-dir-ns-1", "unspecified-dir-ns-2"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -453,7 +491,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByUnspecifiedDirection_Default } func (s *NamespacesSuite) Test_ListNamespaces_SortByBothUnspecified_DefaultsToCreatedAtDESC() { - ids := s.createSortTestNamespaces("both-unspecified-ns") + ids := s.createSortTestNamespaces([]string{"both-unspecified-ns-0", "both-unspecified-ns-1", "both-unspecified-ns-2"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -469,7 +508,8 @@ func (s *NamespacesSuite) Test_ListNamespaces_SortByBothUnspecified_DefaultsToCr } func (s *NamespacesSuite) Test_ListNamespaces_SortOmitted() { - ids := s.createSortTestNamespaces("sort-omitted-ns") + ids := s.createSortTestNamespaces([]string{"sort-omitted-ns-0", "sort-omitted-ns-1", "sort-omitted-ns-2"}) + defer s.deleteSortTestNamespaces(ids) listRsp, err := s.db.PolicyClient.ListNamespaces(s.ctx, &namespaces.ListNamespacesRequest{ State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, @@ -1404,16 +1444,15 @@ func (s *NamespacesSuite) Test_GetNamespace_ByIdAndName_ReturnSameResult() { } } -// createSortTestNamespaces creates 3 namespaces with 5ms gaps for distinct timestamps. -// Returns the namespace IDs in creation order. -func (s *NamespacesSuite) createSortTestNamespaces(label string) []string { - const count = 3 - ids := make([]string, count) - for i := range count { +// createSortTestNamespaces creates namespaces with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the namespace IDs in creation order. +func (s *NamespacesSuite) createSortTestNamespaces(prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { if i > 0 { time.Sleep(5 * time.Millisecond) } - name := fmt.Sprintf("%s-%d-%d.com", label, i, time.Now().UnixNano()) + name := fmt.Sprintf("%s-%d.com", prefix, time.Now().UnixNano()) created, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: name}) s.Require().NoError(err) ids[i] = created.GetId() @@ -1421,18 +1460,9 @@ func (s *NamespacesSuite) createSortTestNamespaces(label string) []string { return ids } -// createNamedSortTestNamespaces creates namespaces with specific name prefixes for name/fqn sort testing. -// Returns the namespace IDs in the same order as the prefixes. -func (s *NamespacesSuite) createNamedSortTestNamespaces(prefixes []string) []string { - suffix := time.Now().UnixNano() - ids := make([]string, len(prefixes)) - for i, prefix := range prefixes { - name := fmt.Sprintf("%s-%d.com", prefix, suffix) - created, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{Name: name}) - s.Require().NoError(err) - ids[i] = created.GetId() - } - return ids +// deleteSortTestNamespaces deactivates namespaces created by sort tests. +func (s *NamespacesSuite) deleteSortTestNamespaces(ids []string) { + s.Require().NoError(forceDeleteRows(s.ctx, s.db, "attribute_namespaces", ids)) } func (s *NamespacesSuite) getActiveNamespaceFixtures() []fixtures.FixtureDataNamespace { diff --git a/service/integration/obligations_test.go b/service/integration/obligations_test.go index e05d6766a2..f950788c35 100644 --- a/service/integration/obligations_test.go +++ b/service/integration/obligations_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" "strconv" "testing" "time" @@ -1724,7 +1725,7 @@ func (s *ObligationsSuite) Test_GetObligation_ByIdAndFqn_ReturnSameResult() { // Sort by Name func (s *ObligationsSuite) Test_ListObligations_SortByName_ASC() { - ids := s.createNamedSortTestObligations([]string{"aaa-sort", "bbb-sort", "ccc-sort"}) + ids := s.createSortTestObligations([]string{"aaa-sort", "bbb-sort", "ccc-sort"}) defer s.deleteObligations(ids) listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ @@ -1740,7 +1741,7 @@ func (s *ObligationsSuite) Test_ListObligations_SortByName_ASC() { } func (s *ObligationsSuite) Test_ListObligations_SortByName_DESC() { - ids := s.createNamedSortTestObligations([]string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) + ids := s.createSortTestObligations([]string{"aaa-sortdesc", "bbb-sortdesc", "ccc-sortdesc"}) defer s.deleteObligations(ids) listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ @@ -1808,7 +1809,7 @@ func (s *ObligationsSuite) Test_ListObligations_SortByFqn_DESC() { // Sort by CreatedAt func (s *ObligationsSuite) Test_ListObligations_SortByCreatedAt_ASC() { - ids := s.createSortTestObligations("createdasc-obl") + ids := s.createSortTestObligations([]string{"createdasc-obl-0", "createdasc-obl-1", "createdasc-obl-2"}) defer s.deleteObligations(ids) listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ @@ -1824,7 +1825,7 @@ func (s *ObligationsSuite) Test_ListObligations_SortByCreatedAt_ASC() { } func (s *ObligationsSuite) Test_ListObligations_SortByCreatedAt_DESC() { - ids := s.createSortTestObligations("createddesc-obl") + ids := s.createSortTestObligations([]string{"createddesc-obl-0", "createddesc-obl-1", "createddesc-obl-2"}) defer s.deleteObligations(ids) listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ @@ -1842,7 +1843,7 @@ func (s *ObligationsSuite) Test_ListObligations_SortByCreatedAt_DESC() { // Sort by UpdatedAt func (s *ObligationsSuite) Test_ListObligations_SortByUpdatedAt_DESC() { - ids := s.createSortTestObligations("upd-sort-obl") + ids := s.createSortTestObligations([]string{"upd-sort-obl-0", "upd-sort-obl-1", "upd-sort-obl-2"}) defer s.deleteObligations(ids) // Update the first obligation so its updated_at is the most recent @@ -1869,7 +1870,7 @@ func (s *ObligationsSuite) Test_ListObligations_SortByUpdatedAt_DESC() { } func (s *ObligationsSuite) Test_ListObligations_SortByUpdatedAt_ASC() { - ids := s.createSortTestObligations("upd-sort-asc-obl") + ids := s.createSortTestObligations([]string{"upd-sort-asc-obl-0", "upd-sort-asc-obl-1", "upd-sort-asc-obl-2"}) defer s.deleteObligations(ids) // Update the last obligation so its updated_at is the most recent @@ -1897,8 +1898,34 @@ func (s *ObligationsSuite) Test_ListObligations_SortByUpdatedAt_ASC() { // Sort by Unspecified (fallback to default) +func (s *ObligationsSuite) Test_ListObligations_SortTieBreaker_CreatedAtWithIDFallback() { + namespaceID, _, _ := s.getNamespaceData(nsExampleCom) + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-obl-%d-%d", i, suffix) + obl := s.createObligation(namespaceID, name, nil) + ids[i] = obl.GetId() + } + defer s.deleteObligations(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "obligation_definitions", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ + Sort: []*obligations.ObligationsSort{ + {Field: obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp, func(o *policy.Obligation) string { return o.GetId() }, sorted[0], sorted[1], sorted[2]) +} + func (s *ObligationsSuite) Test_ListObligations_SortByUnspecifiedField_DefaultsToCreatedAt() { - ids := s.createSortTestObligations("unspecified-field-obl") + ids := s.createSortTestObligations([]string{"unspecified-field-obl-0", "unspecified-field-obl-1", "unspecified-field-obl-2"}) defer s.deleteObligations(ids) listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ @@ -1914,7 +1941,7 @@ func (s *ObligationsSuite) Test_ListObligations_SortByUnspecifiedField_DefaultsT } func (s *ObligationsSuite) Test_ListObligations_SortByUnspecifiedDirection_DefaultsToDESC() { - ids := s.createSortTestObligations("unspecified-dir-obl") + ids := s.createSortTestObligations([]string{"unspecified-dir-obl-0", "unspecified-dir-obl-1", "unspecified-dir-obl-2"}) defer s.deleteObligations(ids) listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ @@ -1930,7 +1957,7 @@ func (s *ObligationsSuite) Test_ListObligations_SortByUnspecifiedDirection_Defau } func (s *ObligationsSuite) Test_ListObligations_SortByBothUnspecified_DefaultsToCreatedAtDESC() { - ids := s.createSortTestObligations("both-unspecified-obl") + ids := s.createSortTestObligations([]string{"both-unspecified-obl-0", "both-unspecified-obl-1", "both-unspecified-obl-2"}) defer s.deleteObligations(ids) listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{ @@ -1946,7 +1973,7 @@ func (s *ObligationsSuite) Test_ListObligations_SortByBothUnspecified_DefaultsTo } func (s *ObligationsSuite) Test_ListObligations_SortOmitted() { - ids := s.createSortTestObligations("sort-omitted-obl") + ids := s.createSortTestObligations([]string{"sort-omitted-obl-0", "sort-omitted-obl-1", "sort-omitted-obl-2"}) defer s.deleteObligations(ids) listRsp, _, err := s.db.PolicyClient.ListObligations(s.ctx, &obligations.ListObligationsRequest{}) @@ -2204,31 +2231,16 @@ func (s *ObligationsSuite) assertObligationValuesSpecificTriggers(obl *policy.Ob // Sort test helpers -// createSortTestObligations creates 3 obligations with 5ms gaps for distinct timestamps. -// Returns the obligation IDs in creation order. -func (s *ObligationsSuite) createSortTestObligations(label string) []string { +// createSortTestObligations creates obligations with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the obligation IDs in creation order. +func (s *ObligationsSuite) createSortTestObligations(prefixes []string) []string { namespaceID, _, _ := s.getNamespaceData(nsExampleCom) - const count = 3 - ids := make([]string, count) - for i := range count { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { if i > 0 { time.Sleep(5 * time.Millisecond) } - name := fmt.Sprintf("%s-%d-%d", label, i, time.Now().UnixNano()) - obl := s.createObligation(namespaceID, name, nil) - ids[i] = obl.GetId() - } - return ids -} - -// createNamedSortTestObligations creates obligations with specific name prefixes for name/FQN sort testing. -// Returns the obligation IDs in the same order as the prefixes. -func (s *ObligationsSuite) createNamedSortTestObligations(prefixes []string) []string { - namespaceID, _, _ := s.getNamespaceData(nsExampleCom) - suffix := time.Now().UnixNano() - ids := make([]string, len(prefixes)) - for i, prefix := range prefixes { - name := fmt.Sprintf("%s-%d", prefix, suffix) + name := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) obl := s.createObligation(namespaceID, name, nil) ids[i] = obl.GetId() } diff --git a/service/integration/registered_resources_test.go b/service/integration/registered_resources_test.go index c744cfebd1..804b29d923 100644 --- a/service/integration/registered_resources_test.go +++ b/service/integration/registered_resources_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" "strings" "testing" "time" @@ -2223,7 +2224,7 @@ func (s *RegisteredResourcesSuite) Test_GetRegisteredResource_ByName_Ambiguous_R // Sort tests func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByName_ASC() { - ids := s.createNamedSortTestRegisteredResources([]string{"aaa-rrsort", "bbb-rrsort", "ccc-rrsort"}) + ids := s.createSortTestRegisteredResources([]string{"aaa-rrsort", "bbb-rrsort", "ccc-rrsort"}) defer s.deleteSortTestRegisteredResources(ids) list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ @@ -2239,7 +2240,7 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByName_ASC() } func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByName_DESC() { - ids := s.createNamedSortTestRegisteredResources([]string{"aaa-rrsortdesc", "bbb-rrsortdesc", "ccc-rrsortdesc"}) + ids := s.createSortTestRegisteredResources([]string{"aaa-rrsortdesc", "bbb-rrsortdesc", "ccc-rrsortdesc"}) defer s.deleteSortTestRegisteredResources(ids) list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ @@ -2255,7 +2256,7 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByName_DESC( } func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByCreatedAt_ASC() { - ids := s.createSortTestRegisteredResources("createdasc-rr") + ids := s.createSortTestRegisteredResources([]string{"createdasc-rr-0", "createdasc-rr-1", "createdasc-rr-2"}) defer s.deleteSortTestRegisteredResources(ids) list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ @@ -2271,7 +2272,7 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByCreatedAt_ } func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByCreatedAt_DESC() { - ids := s.createSortTestRegisteredResources("createddesc-rr") + ids := s.createSortTestRegisteredResources([]string{"createddesc-rr-0", "createddesc-rr-1", "createddesc-rr-2"}) defer s.deleteSortTestRegisteredResources(ids) list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ @@ -2287,7 +2288,7 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByCreatedAt_ } func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUpdatedAt_DESC() { - ids := s.createSortTestRegisteredResources("upd-sort-rr") + ids := s.createSortTestRegisteredResources([]string{"upd-sort-rr-0", "upd-sort-rr-1", "upd-sort-rr-2"}) defer s.deleteSortTestRegisteredResources(ids) // Update the first resource so its updated_at is the most recent @@ -2314,7 +2315,7 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUpdatedAt_ } func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUpdatedAt_ASC() { - ids := s.createSortTestRegisteredResources("upd-sort-asc-rr") + ids := s.createSortTestRegisteredResources([]string{"upd-sort-asc-rr-0", "upd-sort-asc-rr-1", "upd-sort-asc-rr-2"}) defer s.deleteSortTestRegisteredResources(ids) // Update the last resource so its updated_at is the most recent @@ -2340,8 +2341,37 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUpdatedAt_ assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, ids[0], ids[1], ids[2]) } +func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortTieBreaker_CreatedAtWithIDFallback() { + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("tiebreaker-rr-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ + NamespaceId: s.getNamespaceID("example.com"), + Name: name, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestRegisteredResources(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "registered_resources", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Sort: []*registeredresources.RegisteredResourcesSort{ + {Field: registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(list) + + assertIDsInOrder(s.T(), list.GetResources(), func(r *policy.RegisteredResource) string { return r.GetId() }, sorted[0], sorted[1], sorted[2]) +} + func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUnspecifiedField_DefaultsToCreatedAt() { - ids := s.createSortTestRegisteredResources("unspecified-field-rr") + ids := s.createSortTestRegisteredResources([]string{"unspecified-field-rr-0", "unspecified-field-rr-1", "unspecified-field-rr-2"}) defer s.deleteSortTestRegisteredResources(ids) list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ @@ -2357,7 +2387,7 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUnspecifie } func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUnspecifiedDirection_DefaultsToDESC() { - ids := s.createSortTestRegisteredResources("unspecified-dir-rr") + ids := s.createSortTestRegisteredResources([]string{"unspecified-dir-rr-0", "unspecified-dir-rr-1", "unspecified-dir-rr-2"}) defer s.deleteSortTestRegisteredResources(ids) list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ @@ -2373,7 +2403,7 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByUnspecifie } func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByBothUnspecified_DefaultsToCreatedAtDESC() { - ids := s.createSortTestRegisteredResources("both-unspecified-rr") + ids := s.createSortTestRegisteredResources([]string{"both-unspecified-rr-0", "both-unspecified-rr-1", "both-unspecified-rr-2"}) defer s.deleteSortTestRegisteredResources(ids) list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{ @@ -2389,7 +2419,7 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortByBothUnspec } func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortOmitted() { - ids := s.createSortTestRegisteredResources("sort-omitted-rr") + ids := s.createSortTestRegisteredResources([]string{"sort-omitted-rr-0", "sort-omitted-rr-1", "sort-omitted-rr-2"}) defer s.deleteSortTestRegisteredResources(ids) list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, ®isteredresources.ListRegisteredResourcesRequest{}) @@ -2402,33 +2432,15 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_SortOmitted() { // Sort test helpers -// createSortTestRegisteredResources creates 3 registered resources with 5ms gaps for distinct timestamps. -// Returns the resource IDs in creation order. -func (s *RegisteredResourcesSuite) createSortTestRegisteredResources(label string) []string { - const count = 3 - ids := make([]string, count) - for i := range count { +// createSortTestRegisteredResources creates registered resources with the given prefixes, adding 5ms gaps +// between creations for distinct timestamps. Returns the resource IDs in creation order. +func (s *RegisteredResourcesSuite) createSortTestRegisteredResources(prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { if i > 0 { time.Sleep(5 * time.Millisecond) } - name := fmt.Sprintf("%s-%d-%d", label, i, time.Now().UnixNano()) - created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ - NamespaceId: s.getNamespaceID("example.com"), - Name: name, - }) - s.Require().NoError(err) - ids[i] = created.GetId() - } - return ids -} - -// createNamedSortTestRegisteredResources creates registered resources with specific name prefixes for name sort testing. -// Returns the resource IDs in the same order as the prefixes. -func (s *RegisteredResourcesSuite) createNamedSortTestRegisteredResources(prefixes []string) []string { - suffix := time.Now().UnixNano() - ids := make([]string, len(prefixes)) - for i, prefix := range prefixes { - name := fmt.Sprintf("%s-%d", prefix, suffix) + name := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, ®isteredresources.CreateRegisteredResourceRequest{ NamespaceId: s.getNamespaceID("example.com"), Name: name, diff --git a/service/integration/subject_mappings_test.go b/service/integration/subject_mappings_test.go index 393367c354..5376dc8be6 100644 --- a/service/integration/subject_mappings_test.go +++ b/service/integration/subject_mappings_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" "strings" "testing" "time" @@ -599,7 +600,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_OrdersByCreatedAt_Succee } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByCreatedAt_ASC() { - ids := s.createSortTestSubjectMappings("sort-created-asc") + ids := s.createSortTestSubjectMappings([]string{"sort-created-asc-0", "sort-created-asc-1", "sort-created-asc-2"}) + defer s.deleteSortTestSubjectMappings(ids) listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ Sort: []*subjectmapping.SubjectMappingsSort{ @@ -614,7 +616,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByCreatedAt_ASC() { } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByCreatedAt_DESC() { - ids := s.createSortTestSubjectMappings("sort-created-desc") + ids := s.createSortTestSubjectMappings([]string{"sort-created-desc-0", "sort-created-desc-1", "sort-created-desc-2"}) + defer s.deleteSortTestSubjectMappings(ids) listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ Sort: []*subjectmapping.SubjectMappingsSort{ @@ -629,7 +632,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByCreatedAt_DESC() { } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUpdatedAt_DESC() { - ids := s.createSortTestSubjectMappings("sort-updated-desc") + ids := s.createSortTestSubjectMappings([]string{"sort-updated-desc-0", "sort-updated-desc-1", "sort-updated-desc-2"}) + defer s.deleteSortTestSubjectMappings(ids) // Update the first mapping so its updated_at is the most recent time.Sleep(5 * time.Millisecond) @@ -655,7 +659,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUpdatedAt_DESC() { } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUpdatedAt_ASC() { - ids := s.createSortTestSubjectMappings("sort-updated-asc") + ids := s.createSortTestSubjectMappings([]string{"sort-updated-asc-0", "sort-updated-asc-1", "sort-updated-asc-2"}) + defer s.deleteSortTestSubjectMappings(ids) // Update the last mapping so its updated_at is the most recent time.Sleep(5 * time.Millisecond) @@ -680,8 +685,60 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUpdatedAt_ASC() { assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, ids[0], ids[1], ids[2]) } +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortTieBreaker_CreatedAtWithIDFallback() { + fixtureAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value2").ID + actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) + + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + email := fmt.Sprintf("tiebreaker-sm-%d-%d@example.com", i, suffix) + scs := &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".email", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{email}, + }, + }, + }, + }, + }, + }, + } + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fixtureAttrValID, + NewSubjectConditionSet: scs, + Actions: []*policy.Action{actionRead}, + }) + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestSubjectMappings(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "subject_mappings", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetSubjectMappings(), func(sm *policy.SubjectMapping) string { return sm.GetId() }, sorted[0], sorted[1], sorted[2]) +} + func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUnspecifiedField_DefaultsToCreatedAt() { - ids := s.createSortTestSubjectMappings("unspecified-field-sm") + ids := s.createSortTestSubjectMappings([]string{"unspecified-field-sm-0", "unspecified-field-sm-1", "unspecified-field-sm-2"}) + defer s.deleteSortTestSubjectMappings(ids) listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ Sort: []*subjectmapping.SubjectMappingsSort{ @@ -696,7 +753,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUnspecifiedField_D } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUnspecifiedDirection_DefaultsToDESC() { - ids := s.createSortTestSubjectMappings("unspecified-dir-sm") + ids := s.createSortTestSubjectMappings([]string{"unspecified-dir-sm-0", "unspecified-dir-sm-1", "unspecified-dir-sm-2"}) + defer s.deleteSortTestSubjectMappings(ids) listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ Sort: []*subjectmapping.SubjectMappingsSort{ @@ -711,7 +769,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByUnspecifiedDirecti } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByBothUnspecified_DefaultsToCreatedAtDESC() { - ids := s.createSortTestSubjectMappings("both-unspecified-sm") + ids := s.createSortTestSubjectMappings([]string{"both-unspecified-sm-0", "both-unspecified-sm-1", "both-unspecified-sm-2"}) + defer s.deleteSortTestSubjectMappings(ids) listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ Sort: []*subjectmapping.SubjectMappingsSort{ @@ -726,7 +785,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortByBothUnspecified_De } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SortOmitted() { - ids := s.createSortTestSubjectMappings("sort-omitted-sm") + ids := s.createSortTestSubjectMappings([]string{"sort-omitted-sm-0", "sort-omitted-sm-1", "sort-omitted-sm-2"}) + defer s.deleteSortTestSubjectMappings(ids) listRsp, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{}) s.Require().NoError(err) @@ -1423,7 +1483,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_NoNamespaceFilter_R } func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByCreatedAt_ASC() { - ids := s.createSortTestSubjectConditionSets("sort-scs-created-asc") + ids := s.createSortTestSubjectConditionSets([]string{"sort-scs-created-asc-0", "sort-scs-created-asc-1", "sort-scs-created-asc-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ Sort: []*subjectmapping.SubjectConditionSetsSort{ @@ -1438,7 +1499,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByCreatedAt_ASC } func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByCreatedAt_DESC() { - ids := s.createSortTestSubjectConditionSets("sort-scs-created-desc") + ids := s.createSortTestSubjectConditionSets([]string{"sort-scs-created-desc-0", "sort-scs-created-desc-1", "sort-scs-created-desc-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ Sort: []*subjectmapping.SubjectConditionSetsSort{ @@ -1453,7 +1515,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByCreatedAt_DES } func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUpdatedAt_DESC() { - ids := s.createSortTestSubjectConditionSets("sort-scs-updated-desc") + ids := s.createSortTestSubjectConditionSets([]string{"sort-scs-updated-desc-0", "sort-scs-updated-desc-1", "sort-scs-updated-desc-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) // Update the first SCS so its updated_at is the most recent time.Sleep(5 * time.Millisecond) @@ -1479,7 +1542,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUpdatedAt_DES } func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUpdatedAt_ASC() { - ids := s.createSortTestSubjectConditionSets("sort-scs-updated-asc") + ids := s.createSortTestSubjectConditionSets([]string{"sort-scs-updated-asc-0", "sort-scs-updated-asc-1", "sort-scs-updated-asc-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) // Update the last SCS so its updated_at is the most recent time.Sleep(5 * time.Millisecond) @@ -1504,8 +1568,52 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUpdatedAt_ASC assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, ids[0], ids[1], ids[2]) } +func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortTieBreaker_CreatedAtWithIDFallback() { + suffix := time.Now().UnixNano() + ids := make([]string, 3) + for i := range 3 { + val := fmt.Sprintf("tiebreaker-scs-%d-%d", i, suffix) + created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".sort_test", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{val}, + }, + }, + }, + }, + }, + }, + }, "", "") + s.Require().NoError(err) + ids[i] = created.GetId() + } + defer s.deleteSortTestSubjectConditionSets(ids) + + s.Require().NoError(forceCreatedAtTie(s.ctx, s.db, "subject_condition_set", ids)) + + sorted := slices.Sorted(slices.Values(ids)) + + listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ + Sort: []*subjectmapping.SubjectConditionSetsSort{ + {Field: subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.NotNil(listRsp) + + assertIDsInOrder(s.T(), listRsp.GetSubjectConditionSets(), func(scs *policy.SubjectConditionSet) string { return scs.GetId() }, sorted[0], sorted[1], sorted[2]) +} + func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUnspecifiedField_DefaultsToCreatedAt() { - ids := s.createSortTestSubjectConditionSets("unspecified-field-scs") + ids := s.createSortTestSubjectConditionSets([]string{"unspecified-field-scs-0", "unspecified-field-scs-1", "unspecified-field-scs-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ Sort: []*subjectmapping.SubjectConditionSetsSort{ @@ -1520,7 +1628,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUnspecifiedFi } func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUnspecifiedDirection_DefaultsToDESC() { - ids := s.createSortTestSubjectConditionSets("unspecified-dir-scs") + ids := s.createSortTestSubjectConditionSets([]string{"unspecified-dir-scs-0", "unspecified-dir-scs-1", "unspecified-dir-scs-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ Sort: []*subjectmapping.SubjectConditionSetsSort{ @@ -1535,7 +1644,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByUnspecifiedDi } func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByBothUnspecified_DefaultsToCreatedAtDESC() { - ids := s.createSortTestSubjectConditionSets("both-unspecified-scs") + ids := s.createSortTestSubjectConditionSets([]string{"both-unspecified-scs-0", "both-unspecified-scs-1", "both-unspecified-scs-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{ Sort: []*subjectmapping.SubjectConditionSetsSort{ @@ -1550,7 +1660,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortByBothUnspecifi } func (s *SubjectMappingsSuite) Test_ListSubjectConditionSets_SortOmitted() { - ids := s.createSortTestSubjectConditionSets("sort-omitted-scs") + ids := s.createSortTestSubjectConditionSets([]string{"sort-omitted-scs-0", "sort-omitted-scs-1", "sort-omitted-scs-2"}) + defer s.deleteSortTestSubjectConditionSets(ids) listRsp, err := s.db.PolicyClient.ListSubjectConditionSets(s.ctx, &subjectmapping.ListSubjectConditionSetsRequest{}) s.Require().NoError(err) @@ -2748,19 +2859,16 @@ func (s *SubjectMappingsSuite) newSCSInNamespace(nsID string) *policy.SubjectCon return scs } -// createSortTestSubjectMappings creates 3 subject mappings with 5ms gaps for distinct timestamps. -// Returns the subject mapping IDs in creation order. -func (s *SubjectMappingsSuite) createSortTestSubjectMappings(label string) []string { +func (s *SubjectMappingsSuite) createSortTestSubjectMappings(prefixes []string) []string { fixtureAttrValID := s.f.GetAttributeValueKey("example.net/attr/attr1/value/value2").ID actionRead := s.f.GetStandardAction(policydb.ActionRead.String()) - const count = 3 - ids := make([]string, count) - for i := range count { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { if i > 0 { time.Sleep(5 * time.Millisecond) } - email := fmt.Sprintf("%s-%d-%d@example.com", label, i, time.Now().UnixNano()) + email := fmt.Sprintf("%s-%d@example.com", prefix, time.Now().UnixNano()) scs := &subjectmapping.SubjectConditionSetCreate{ SubjectSets: []*policy.SubjectSet{ { @@ -2790,16 +2898,13 @@ func (s *SubjectMappingsSuite) createSortTestSubjectMappings(label string) []str return ids } -// createSortTestSubjectConditionSets creates 3 subject condition sets with 5ms gaps for distinct timestamps. -// Returns the SCS IDs in creation order. -func (s *SubjectMappingsSuite) createSortTestSubjectConditionSets(label string) []string { - const count = 3 - ids := make([]string, count) - for i := range count { +func (s *SubjectMappingsSuite) createSortTestSubjectConditionSets(prefixes []string) []string { + ids := make([]string, len(prefixes)) + for i, prefix := range prefixes { if i > 0 { time.Sleep(5 * time.Millisecond) } - val := fmt.Sprintf("%s-%d-%d", label, i, time.Now().UnixNano()) + val := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) created, err := s.db.PolicyClient.CreateSubjectConditionSet(s.ctx, &subjectmapping.SubjectConditionSetCreate{ SubjectSets: []*policy.SubjectSet{ { @@ -2823,3 +2928,19 @@ func (s *SubjectMappingsSuite) createSortTestSubjectConditionSets(label string) } return ids } + +// deleteSortTestSubjectMappings cleans up subject mappings created by sort tests. +func (s *SubjectMappingsSuite) deleteSortTestSubjectMappings(ids []string) { + for _, id := range ids { + _, err := s.db.PolicyClient.DeleteSubjectMapping(s.ctx, id) + s.Require().NoError(err) + } +} + +// deleteSortTestSubjectConditionSets cleans up subject condition sets created by sort tests. +func (s *SubjectMappingsSuite) deleteSortTestSubjectConditionSets(ids []string) { + for _, id := range ids { + _, err := s.db.PolicyClient.DeleteSubjectConditionSet(s.ctx, id) + s.Require().NoError(err) + } +} diff --git a/service/integration/utils.go b/service/integration/utils.go index dc40bd518d..27b5121906 100644 --- a/service/integration/utils.go +++ b/service/integration/utils.go @@ -1,11 +1,16 @@ package integration import ( + "context" + "fmt" "testing" + "github.com/opentdf/platform/service/internal/fixtures" "github.com/stretchr/testify/require" ) +// assertIDsInOrder verifies that the given IDs appear in the expected relative +// order within items, tolerating extra rows that don't match any target ID. func assertIDsInOrder[T any](tb testing.TB, items []T, getID func(T) string, ids ...string) { tb.Helper() @@ -27,3 +32,25 @@ func assertIDsInOrder[T any](tb testing.TB, items []T, getID func(T) string, ids require.Less(tb, positions[ids[i]], positions[ids[i+1]]) } } + +// forceDeleteRows hard-deletes rows by ID via raw SQL, bypassing the API's +// soft-delete/deactivate limitation for resources like namespaces and attributes. +func forceDeleteRows(ctx context.Context, db fixtures.DBInterface, table string, ids []string) error { + sql := fmt.Sprintf( + `DELETE FROM %s WHERE id = ANY($1::uuid[])`, + db.TableName(table), + ) + _, err := db.Client.Pgx.Exec(ctx, sql, ids) + return err +} + +// forceCreatedAtTie sets created_at to a fixed timestamp for the given IDs, +// guaranteeing that the ORDER BY tiebreaker (id ASC) determines sort order. +func forceCreatedAtTie(ctx context.Context, db fixtures.DBInterface, table string, ids []string) error { + sql := fmt.Sprintf( + `UPDATE %s SET created_at = '2000-01-01T00:00:00Z' WHERE id = ANY($1::uuid[])`, + db.TableName(table), + ) + _, err := db.Client.Pgx.Exec(ctx, sql, ids) + return err +}