From 2342156b35c443e1e23dbf576065949d62a7095b Mon Sep 17 00:00:00 2001 From: Antoine Popineau Date: Wed, 22 Jan 2025 19:09:47 +0100 Subject: [PATCH] Added example simple usecase test, mocking enforcers and repositories. --- go.mod | 2 + go.sum | 4 ++ .../executor_factory/executor_factory_stub.go | 39 ++++++++++++ usecases/sanction_check_usecase.go | 19 ++++-- usecases/sanction_check_usecase_mock_test.go | 45 ++++++++++++++ usecases/sanction_check_usecase_test.go | 62 +++++++++++++++++++ utils/testing.go | 50 +++++++++++++++ 7 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 usecases/executor_factory/executor_factory_stub.go create mode 100644 usecases/sanction_check_usecase_mock_test.go create mode 100644 usecases/sanction_check_usecase_test.go create mode 100644 utils/testing.go diff --git a/go.mod b/go.mod index 7218c3dfc..af7da5ace 100644 --- a/go.mod +++ b/go.mod @@ -116,6 +116,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/getkin/kin-openapi v0.124.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-faker/faker/v4 v4.5.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect @@ -167,6 +168,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc4 // indirect github.com/opencontainers/runc v1.1.14 // indirect + github.com/pashagolub/pgxmock/v4 v4.4.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect diff --git a/go.sum b/go.sum index 3c7728db5..30744d462 100644 --- a/go.sum +++ b/go.sum @@ -196,6 +196,8 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-faker/faker/v4 v4.5.0 h1:ARzAY2XoOL9tOUK+KSecUQzyXQsUaZHefjyF8x6YFHc= +github.com/go-faker/faker/v4 v4.5.0/go.mod h1:p3oq1GRjG2PZ7yqeFFfQI20Xm61DoBDlCA8RiSyZ48M= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -389,6 +391,8 @@ github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2 github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/pashagolub/pgxmock/v4 v4.4.0 h1:zrZHBzqlzIFrq5Iw6nQpmpEd77eLqGIC2ol4ZTeojz0= +github.com/pashagolub/pgxmock/v4 v4.4.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= diff --git a/usecases/executor_factory/executor_factory_stub.go b/usecases/executor_factory/executor_factory_stub.go new file mode 100644 index 000000000..2d902905c --- /dev/null +++ b/usecases/executor_factory/executor_factory_stub.go @@ -0,0 +1,39 @@ +package executor_factory + +import ( + "context" + + "github.com/checkmarble/marble-backend/models" + "github.com/checkmarble/marble-backend/repositories" + "github.com/pashagolub/pgxmock/v4" +) + +type ExecutorFactoryStub struct { + Mock pgxmock.PgxPoolIface +} + +func NewExecutorFactoryStub() ExecutorFactoryStub { + pool, _ := pgxmock.NewPool() + + return ExecutorFactoryStub{ + Mock: pool, + } +} + +type PgExecutorStub struct { + pgxmock.PgxPoolIface +} + +func (stub ExecutorFactoryStub) NewClientDbExecutor(ctx context.Context, organizationId string) (repositories.Executor, error) { + return nil, nil +} + +func (stub ExecutorFactoryStub) NewExecutor() repositories.Executor { + return PgExecutorStub{ + stub.Mock, + } +} + +func (stub PgExecutorStub) DatabaseSchema() models.DatabaseSchema { + return models.DatabaseSchema{} +} diff --git a/usecases/sanction_check_usecase.go b/usecases/sanction_check_usecase.go index 08d0f331d..b98d5ce9c 100644 --- a/usecases/sanction_check_usecase.go +++ b/usecases/sanction_check_usecase.go @@ -6,11 +6,18 @@ import ( "github.com/checkmarble/marble-backend/models" "github.com/checkmarble/marble-backend/repositories" "github.com/checkmarble/marble-backend/usecases/executor_factory" - "github.com/checkmarble/marble-backend/usecases/security" "github.com/checkmarble/marble-backend/utils" "github.com/pkg/errors" ) +type SanctionCheckEnforceSecurityDecision interface { + ReadDecision(models.Decision) error +} + +type SanctionCheckEnforceSecurityCase interface { + ReadOrUpdateCase(models.Case, []string) error +} + type SanctionCheckProvider interface { Search(context.Context, models.SanctionCheckConfig, models.OpenSanctionsQuery) (models.SanctionCheck, error) @@ -20,6 +27,10 @@ type SanctionCheckDecisionRepository interface { DecisionsById(ctx context.Context, exec repositories.Executor, decisionIds []string) ([]models.Decision, error) } +type SanctionCheckOrganizationRepository interface { + GetOrganizationById(ctx context.Context, exec repositories.Executor, organizationId string) (models.Organization, error) +} + type SanctionCheckRepository interface { ListSanctionChecksForDecision(context.Context, repositories.Executor, string) ([]models.SanctionCheck, error) GetSanctionCheck(context.Context, repositories.Executor, string) (models.SanctionCheck, error) @@ -38,10 +49,10 @@ type SanctionCheckRepository interface { } type SanctionCheckUsecase struct { - enforceSecurityDecision security.EnforceSecurityDecision - enforceSecurityCase security.EnforceSecurityCase + enforceSecurityDecision SanctionCheckEnforceSecurityDecision + enforceSecurityCase SanctionCheckEnforceSecurityCase - organizationRepository repositories.OrganizationRepository + organizationRepository SanctionCheckOrganizationRepository decisionRepository SanctionCheckDecisionRepository openSanctionsProvider SanctionCheckProvider repository SanctionCheckRepository diff --git a/usecases/sanction_check_usecase_mock_test.go b/usecases/sanction_check_usecase_mock_test.go new file mode 100644 index 000000000..5a8d551df --- /dev/null +++ b/usecases/sanction_check_usecase_mock_test.go @@ -0,0 +1,45 @@ +package usecases + +import ( + "context" + + "github.com/checkmarble/marble-backend/models" + "github.com/checkmarble/marble-backend/repositories" + "github.com/checkmarble/marble-backend/utils" +) + +type sanctionCheckEnforcerMock struct{} + +func (sanctionCheckEnforcerMock) ReadDecision(models.Decision) error { + return nil +} + +func (sanctionCheckEnforcerMock) ReadOrUpdateCase(models.Case, []string) error { + return nil +} + +type sanctionCheckRepositoryMock struct{} + +func (sanctionCheckRepositoryMock) GetOrganizationById(ctx context.Context, + exec repositories.Executor, organizationId string, +) (models.Organization, error) { + return models.Organization{ + Id: "orgid", + Name: "ACME Inc.", + OpenSanctionsConfig: models.OrganizationOpenSanctionsConfig{ + Datasets: []string{"ds1", "ds2"}, + MatchThreshold: utils.Ptr(42), + MatchLimit: utils.Ptr(10), + }, + }, nil +} + +func (sanctionCheckRepositoryMock) DecisionsById(ctx context.Context, exec repositories.Executor, decisionIds []string) ([]models.Decision, error) { + decisions := []models.Decision{ + { + DecisionId: "decisionid", + }, + } + + return decisions, nil +} diff --git a/usecases/sanction_check_usecase_test.go b/usecases/sanction_check_usecase_test.go new file mode 100644 index 000000000..0d2b67e6c --- /dev/null +++ b/usecases/sanction_check_usecase_test.go @@ -0,0 +1,62 @@ +package usecases + +import ( + "context" + "testing" + + "github.com/checkmarble/marble-backend/repositories" + "github.com/checkmarble/marble-backend/repositories/dbmodels" + "github.com/checkmarble/marble-backend/usecases/executor_factory" + "github.com/checkmarble/marble-backend/utils" + "github.com/pashagolub/pgxmock/v4" + "github.com/stretchr/testify/assert" +) + +func buildUsecase() (SanctionCheckUsecase, executor_factory.ExecutorFactoryStub) { + enforceSecurity := sanctionCheckEnforcerMock{} + mock := sanctionCheckRepositoryMock{} + exec := executor_factory.NewExecutorFactoryStub() + + uc := SanctionCheckUsecase{ + enforceSecurityDecision: enforceSecurity, + enforceSecurityCase: enforceSecurity, + organizationRepository: mock, + decisionRepository: mock, + repository: &repositories.MarbleDbRepository{}, + executorFactory: exec, + } + + return uc, exec +} + +func TestGetSanctionCheckOnDecision(t *testing.T) { + uc, exec := buildUsecase() + mockSc, mockScRow := utils.FakeStruct[dbmodels.DBSanctionCheck]() + mockScMatch, mockScMatchRow := utils.FakeStruct[dbmodels.DBSanctionCheckMatchWithComments]() + + exec.Mock.ExpectQuery(`SELECT .* FROM sanction_checks WHERE decision_id = \$1`). + WithArgs("decisionid"). + WillReturnRows( + pgxmock.NewRows(dbmodels.SelectSanctionChecksColumn). + AddRow(mockScRow...), + ) + + exec.Mock.ExpectQuery(`SELECT .* FROM sanction_check_matches matches LEFT JOIN sanction_check_match_comments comments ON matches.id = comments.sanction_check_match_id WHERE sanction_check_id = \$1 GROUP BY matches.id`). + WithArgs(mockSc.Id). + WillReturnRows( + pgxmock.NewRows(utils.ColumnList[dbmodels.DBSanctionCheckMatchWithComments]()). + AddRow(mockScMatchRow...). + AddRow(utils.FakeStructRow[dbmodels.DBSanctionCheckMatchWithComments]()...). + AddRow(utils.FakeStructRow[dbmodels.DBSanctionCheckMatchWithComments]()...), + ) + + scs, err := uc.ListSanctionChecks(context.TODO(), "decisionid") + + assert.NoError(t, exec.Mock.ExpectationsWereMet()) + assert.NoError(t, err) + assert.Len(t, scs, 1) + assert.Equal(t, mockSc.Status, scs[0].Status) + assert.Len(t, scs[0].Matches, 3) + assert.Equal(t, mockScMatch.Status, scs[0].Matches[0].Status) + assert.Equal(t, mockScMatch.CommentCount, scs[0].Matches[0].CommentCount) +} diff --git a/utils/testing.go b/utils/testing.go new file mode 100644 index 000000000..eabf1d800 --- /dev/null +++ b/utils/testing.go @@ -0,0 +1,50 @@ +package utils + +import ( + "reflect" + + "github.com/go-faker/faker/v4" +) + +func FakeStruct[T any]() (T, []any) { + var object T + + _ = faker.FakeData(&object) + + return object, StructToMockRow(object) +} + +func FakeStructRow[T any]() []any { + _, row := FakeStruct[T]() + + return row +} + +func StructToMockRow[T any](object T) []any { + f := reflect.ValueOf(object) + t := reflect.TypeOf(object) + + if f.Kind() != reflect.Struct { + panic("StructToMockRow should only be used on structs") + } + + slice := make([]any, 0) + + for i := 0; i < f.NumField(); i++ { + sf := f.Field(i) + + switch sf.Kind() { + case reflect.Struct: + switch t.Field(i).Anonymous { + case true: + slice = append(slice, StructToMockRow(sf.Interface())...) + default: + slice = append(slice, sf.Interface()) + } + default: + slice = append(slice, sf.Interface()) + } + } + + return slice +}