Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion integration/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type FixtureData struct {
Namespaces struct {
Metadata FixtureMetadata `yaml:"metadata"`
Data map[string]FixtureDataNamespace `yaml:"data"`
} `yaml:"namespaces"`
} `yaml:"attribute_namespaces"`
Attributes struct {
Metadata FixtureMetadata `yaml:"metadata"`
Data map[string]FixtureDataAttribute `yaml:"data"`
Expand Down
24 changes: 12 additions & 12 deletions integration/fixtures.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
##
# Namespaces
##
namespaces:
metadata:
table_name: namespaces
columns:
attribute_namespaces:
metadata:
table_name: attribute_namespaces
columns:
- id
- name
data:
Expand All @@ -24,9 +24,9 @@ namespaces:
# Attribute Rule Enum: UNSPECIFIED, ANY_OF, ALL_OF, HIERARCHY
##
attributes:
metadata:
metadata:
table_name: attribute_definitions
columns:
columns:
- id
- namespace_id
- name
Expand Down Expand Up @@ -79,9 +79,9 @@ attributes:
# Attribute Values
##
attribute_values:
metadata:
metadata:
table_name: attribute_values
columns:
columns:
- id
- attribute_definition_id
- value
Expand Down Expand Up @@ -119,15 +119,15 @@ attribute_values:
attribute_definition_id: 00000000-0000-0000-0000-000000000002
value: value2

##
##
# Subject Mappings
#
# Operator Enum: UNSPECIFIED, IN, NOT_IN
##
subject_mappings:
metadata:
metadata:
table_name: subject_mappings
columns:
columns:
- id
- attribute_value_id
- operator
Expand All @@ -151,4 +151,4 @@ subject_mappings:
subject_attribute_values:
- value1
- value2
- value3
- value3
151 changes: 151 additions & 0 deletions integration/namespaces_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package integration

import (
"context"
"fmt"
"log/slog"
"strings"
"testing"

"github.com/opentdf/opentdf-v2-poc/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)

type NamespacesSuite struct {
suite.Suite
schema string
f Fixtures
db DBInterface
ctx context.Context
}

const nonExistantNamespaceId = "88888888-2222-3333-4444-999999999999"

func (s *NamespacesSuite) SetupSuite() {
slog.Info("setting up db.Namespaces test suite")
s.ctx = context.Background()
s.schema = "test_opentdf_namespaces"
s.db = NewDBInterface(s.schema)
s.f = NewFixture(s.db)
s.f.Provision()
}

func (s *NamespacesSuite) TearDownSuite() {
slog.Info("tearing down db.Namespaces test suite")
s.f.TearDown()
}

func getNamespaceFixtures() []FixtureDataNamespace {
return []FixtureDataNamespace{
fixtures.GetNamespaceKey("example.com"),
fixtures.GetNamespaceKey("example.net"),
fixtures.GetNamespaceKey("example.org"),
}
}

func (s *NamespacesSuite) Test_CreateNamespace() {
testData := getNamespaceFixtures()

for _, ns := range testData {
ns.Name = strings.Replace(ns.Name, "example", "test", 1)
createdNamespace, err := s.db.Client.CreateNamespace(s.ctx, ns.Name)
assert.Nil(s.T(), err)
assert.NotNil(s.T(), createdNamespace)
}

// Creating a namespace with a name conflict should fail
for _, ns := range testData {
_, err := s.db.Client.CreateNamespace(s.ctx, ns.Name)
assert.NotNil(s.T(), err)
assert.ErrorIs(s.T(), err, db.ErrUniqueConstraintViolation)
}
}

func (s *NamespacesSuite) Test_GetNamespace() {
testData := getNamespaceFixtures()

for _, test := range testData {
gotNamespace, err := s.db.Client.GetNamespace(s.ctx, test.Id)
assert.Nil(s.T(), err)
assert.NotNil(s.T(), gotNamespace)
// name retrieved by ID equal to name used to create
assert.Equal(s.T(), test.Name, gotNamespace.Name)
}

// Getting a namespace with an nonexistant id should fail
_, err := s.db.Client.GetNamespace(s.ctx, nonExistantNamespaceId)
assert.NotNil(s.T(), err)
assert.ErrorIs(s.T(), err, db.ErrNotFound)
}

func (s *NamespacesSuite) Test_ListNamespaces() {
testData := getNamespaceFixtures()

gotNamespaces, err := s.db.Client.ListNamespaces(s.ctx)
assert.Nil(s.T(), err)
assert.NotNil(s.T(), gotNamespaces)
assert.GreaterOrEqual(s.T(), len(gotNamespaces), len(testData))
}

func (s *NamespacesSuite) Test_UpdateNamespace() {
testData := getNamespaceFixtures()

for i, ns := range testData {
updatedName := fmt.Sprintf("%s-updated", ns.Name)
testData[i].Name = updatedName
updatedNamespace, err := s.db.Client.UpdateNamespace(s.ctx, ns.Id, updatedName)
assert.Nil(s.T(), err)
assert.NotNil(s.T(), updatedNamespace)
assert.Equal(s.T(), updatedName, updatedNamespace.Name)
}

// Update when the namespace does not exist should fail
_, err := s.db.Client.UpdateNamespace(s.ctx, nonExistantNamespaceId, "new-namespace.com")
assert.NotNil(s.T(), err)
assert.ErrorIs(s.T(), err, db.ErrNotFound)

// Update to a conflict should fail
gotNamespace, e := s.db.Client.UpdateNamespace(s.ctx, testData[0].Id, testData[1].Name)
assert.Nil(s.T(), gotNamespace)
assert.NotNil(s.T(), e)
assert.ErrorIs(s.T(), e, db.ErrUniqueConstraintViolation)
}

func (s *NamespacesSuite) Test_DeleteNamespace() {
testData := getNamespaceFixtures()

// Deletion should fail when the namespace is referenced as FK in attribute(s)
for _, ns := range testData {
err := s.db.Client.DeleteNamespace(s.ctx, ns.Id)
assert.NotNil(s.T(), err)
assert.ErrorIs(s.T(), err, db.ErrForeignKeyViolation)
}

// Deletion should succeed when NOT referenced as FK in attribute(s)
newNamespaceId, err := s.db.Client.CreateNamespace(s.ctx, "new-namespace.com")
assert.Nil(s.T(), err)
assert.NotEqual(s.T(), "", newNamespaceId)

err = s.db.Client.DeleteNamespace(s.ctx, newNamespaceId)
assert.Nil(s.T(), err)

// Deleted namespace should not be found on List
gotNamespaces, err := s.db.Client.ListNamespaces(s.ctx)
assert.Nil(s.T(), err)
assert.NotNil(s.T(), gotNamespaces)
for _, ns := range gotNamespaces {
assert.NotEqual(s.T(), newNamespaceId, ns.Id)
}

// Deleted namespace should not be found on Get
_, err = s.db.Client.GetNamespace(s.ctx, newNamespaceId)
assert.NotNil(s.T(), err)
}

func TestNamespacesSuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping namespaces integration tests")
}
suite.Run(t, new(NamespacesSuite))
}
5 changes: 5 additions & 0 deletions internal/db/db_migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ func (c *Client) RunMigrations() (int, error) {
applied int
)

// create the schema
c.Exec(context.Background(), fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", c.config.Schema))
// set the search path
c.Exec(context.Background(), fmt.Sprintf("SET search_path TO %s", c.config.Schema))

if !c.config.RunMigrations {
slog.Info("skipping migrations",
slog.String("reason", "runMigrations is false"),
Expand Down
12 changes: 7 additions & 5 deletions internal/db/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package db
import (
"errors"
"fmt"
"log/slog"
"strings"

"github.com/jackc/pgerrcode"
Expand All @@ -16,11 +17,11 @@ func (e DbError) Error() string {
}

const (
ErrUniqueConstraintViolation DbError = "error: value must be unique"
ErrNotNullViolation DbError = "error: value cannot be null"
ErrForeignKeyViolation DbError = "error: value must exist in another table"
ErrRestrictViolation DbError = "error: value cannot be deleted due to restriction"
ErrNotFound DbError = "error: value not found"
ErrUniqueConstraintViolation DbError = "ErrUniqueConstraintViolation: value must be unique"
ErrNotNullViolation DbError = "ErrNotNullViolation: value cannot be null"
ErrForeignKeyViolation DbError = "ErrForeignKeyViolation: value is referenced by another table"
ErrRestrictViolation DbError = "ErrRestrictViolation: value cannot be deleted due to restriction"
ErrNotFound DbError = "ErrNotFound: value not found"
)

// Validate is a PostgreSQL constraint violation for specific table-column value
Expand All @@ -36,6 +37,7 @@ func IsConstraintViolationForColumnVal(err error, table string, column string) b
// Get helpful error message for PostgreSQL violation
func WrapIfKnownInvalidQueryErr(err error) error {
if e := isPgError(err); e != nil {
slog.Error("Encountered database error", slog.String("error", e.Error()))
switch e.Code {
case pgerrcode.UniqueViolation:
return errors.Join(ErrUniqueConstraintViolation, e)
Expand Down
13 changes: 12 additions & 1 deletion internal/db/namespaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func (c Client) UpdateNamespace(ctx context.Context, id string, name string) (*n
}

func deleteNamespaceSql(id string) (string, []interface{}, error) {
// TODO: handle delete cascade, dangerous deletion via special rpc, or "soft-delete" status change
return newStatementBuilder().
Delete(NamespacesTable).
Where(sq.Eq{"id": id}).
Expand All @@ -153,5 +154,15 @@ func deleteNamespaceSql(id string) (string, []interface{}, error) {
func (c Client) DeleteNamespace(ctx context.Context, id string) error {
sql, args, err := deleteNamespaceSql(id)

return c.exec(ctx, sql, args, err)
e := c.exec(ctx, sql, args, err)
if e != nil {
if errors.Is(e, ErrNotFound) {
slog.Error(services.ErrNotFound, slog.String("error", e.Error()))
} else if errors.Is(e, ErrForeignKeyViolation) {
slog.Error(services.ErrConflict, slog.String("error", e.Error()))
} else {
slog.Error(services.ErrDeletingResource, slog.String("error", e.Error()))
}
}
return e
}