From 593bc9715ce7dfc2a3dfae16add808050516240f Mon Sep 17 00:00:00 2001 From: Vasiliy Vasilyuk Date: Tue, 5 Mar 2024 21:09:21 +0300 Subject: [PATCH] Add example of tests with isolated schema --- README.md | 1 + go.mod | 9 +- go.sum | 53 +++++++- migrations/embed.go | 10 ++ testingpg/testingpg.go | 50 +++++++ testingpg/testingpg_test.go | 80 +++++++++++ user_repository.go | 3 +- user_repository_test.go | 1 + user_repository_with_isolated_schema_test.go | 134 +++++++++++++++++++ 9 files changed, 333 insertions(+), 8 deletions(-) create mode 100644 migrations/embed.go create mode 100644 user_repository_with_isolated_schema_test.go diff --git a/README.md b/README.md index 50a97e2..8d6b0a1 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ total: (statements) 100.0% package. - [Example of integration testing with isolated database for each testcase](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_isolated_database_test.go). - [Example of integration testing with transaction cleanup for each testcase](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_transactional_cleanup_test.go). +- [Example of integration testing with isolated schema for each testcase](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_isolated_schema_test.go). - And example of [GitHub Actions](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/.github/workflows/go.yml) and [Gitlab CI](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/.gitlab-ci.yml). diff --git a/go.mod b/go.mod index 64d7fd9..41e364a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/xorcare/testing-go-code-with-postgres go 1.22.0 require ( + github.com/golang-migrate/migrate/v4 v4.17.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.6.0 github.com/stretchr/testify v1.9.0 @@ -10,14 +11,18 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sync v0.1.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/crypto v0.20.0 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index dad6ff3..f047e28 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,32 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= +github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= +github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -16,6 +39,18 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -25,12 +60,22 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/migrations/embed.go b/migrations/embed.go new file mode 100644 index 0000000..12cc1e9 --- /dev/null +++ b/migrations/embed.go @@ -0,0 +1,10 @@ +// Copyright (c) 2024 Vasiliy Vasilyuk. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package migrations + +import "embed" + +//go:embed *.up.sql +var FS embed.FS diff --git a/testingpg/testingpg.go b/testingpg/testingpg.go index 49d1343..76d42aa 100644 --- a/testingpg/testingpg.go +++ b/testingpg/testingpg.go @@ -17,6 +17,7 @@ import ( "time" "unicode" + _ "github.com/golang-migrate/migrate/v4/source/file" _ "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/require" ) @@ -37,6 +38,10 @@ func NewWithIsolatedDatabase(t TestingT) *Postgres { return newPostgres(t, defaultPostgresURL).cloneFromReference() } +func NewWithIsolatedSchema(t TestingT) *Postgres { + return newPostgres(t, defaultPostgresURL).createSchema(t) +} + func NewWithTransactionalCleanup(t TestingT) interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row @@ -108,6 +113,40 @@ func (p *Postgres) DB() *sql.DB { return p.sqlDB } +func (p *Postgres) createSchema(t TestingT) *Postgres { + schemaName := newUniqueHumanReadableDatabaseName(p.t) + + // Unclear why, but if the scheme contains letters of different case, the + // tests stop working. At the moment I don't quite understand why this + // happens, but converting to lower case fixes the problem. + schemaName = strings.ToLower(schemaName) + + ctx, done := context.WithCancel(context.Background()) + t.Cleanup(done) + + { + sql := fmt.Sprintf(`CREATE SCHEMA "%s";`, schemaName) + + _, err := p.DB().ExecContext(ctx, sql) + require.NoError(t, err) + } + + t.Cleanup(func() { + sql := fmt.Sprintf(`DROP SCHEMA "%s" CASCADE;`, schemaName) + + _, err := p.DB().ExecContext(ctx, sql) + require.NoError(t, err) + }) + + pgurl := setSearchPath(t, p.URL(), schemaName) + + return &Postgres{ + t: p.t, + ref: p.ref, + url: pgurl.String(), + } +} + func (p *Postgres) cloneFromReference() *Postgres { newDBName := newUniqueHumanReadableDatabaseName(p.t) @@ -220,3 +259,14 @@ func open(t TestingT, dataSourceURL string) *sql.DB { return db } + +func setSearchPath(t TestingT, pgURL string, schemaName string) *url.URL { + pgurl, err := url.Parse(pgURL) + require.NoError(t, err) + + query := pgurl.Query() + query.Set("search_path", schemaName) + pgurl.RawQuery = query.Encode() + + return pgurl +} diff --git a/testingpg/testingpg_test.go b/testingpg/testingpg_test.go index 3fe4a83..8b1dd78 100644 --- a/testingpg/testingpg_test.go +++ b/testingpg/testingpg_test.go @@ -94,6 +94,86 @@ func TestNewPostgres(t *testing.T) { }) } +func TestNewWithIsolatedSchema(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + t.Parallel() + + t.Run("Successfully connect by URL and get version", func(t *testing.T) { + t.Parallel() + + // Arrange + postgres := testingpg.NewWithIsolatedSchema(t) + + ctx := context.Background() + dbPool, err := pgxpool.New(ctx, postgres.URL()) + require.NoError(t, err) + + // Act + var version string + err = dbPool.QueryRow(ctx, "SHOW search_path;").Scan(&version) + + // Assert + require.NoError(t, err) + require.NotEmpty(t, version) + t.Log(version) + }) + + t.Run("Successfully obtained a version using a pre-configured conn", func(t *testing.T) { + t.Parallel() + + // Arrange + postgres := testingpg.NewWithIsolatedSchema(t) + ctx := context.Background() + + // Act + var version string + err := postgres.DB().QueryRowContext(ctx, "SHOW search_path;").Scan(&version) + + // Assert + require.NoError(t, err) + require.NotEmpty(t, version) + + t.Log(version) + }) + + t.Run("Changes are not visible in different instances", func(t *testing.T) { + t.Parallel() + + // Arrange + postgres1 := testingpg.NewWithIsolatedSchema(t) + postgres2 := testingpg.NewWithIsolatedSchema(t) + + ctx := context.Background() + + // Act + const sqlStr = `CREATE TABLE "no_conflict" (id integer PRIMARY KEY)` + _, err1 := postgres1.DB().ExecContext(ctx, sqlStr) + _, err2 := postgres2.DB().ExecContext(ctx, sqlStr) + + // Assert + require.NoError(t, err1) + require.NoError(t, err2, "databases must be isolated for each instance") + }) + + t.Run("URL is different at different instances", func(t *testing.T) { + t.Parallel() + + // Arrange + postgres1 := testingpg.NewWithIsolatedSchema(t) + postgres2 := testingpg.NewWithIsolatedSchema(t) + + // Act + url1 := postgres1.URL() + url2 := postgres2.URL() + + // Assert + require.NotEqual(t, url1, url2) + }) +} + func TestNewWithTransactionalCleanup(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") diff --git a/user_repository.go b/user_repository.go index fd9c5bf..152419d 100644 --- a/user_repository.go +++ b/user_repository.go @@ -34,8 +34,7 @@ func (r *UserRepository) ReadUser(ctx context.Context, userID uuid.UUID) (User, err := row.Scan(&user.ID, &user.Username, &user.CreatedAt) if err != nil { - const format = "failed selection of User from database: %v" - + const format = "failed selection of User from database: %w" return User{}, fmt.Errorf(format, err) } diff --git a/user_repository_test.go b/user_repository_test.go index 753f6c8..35a0a6a 100644 --- a/user_repository_test.go +++ b/user_repository_test.go @@ -9,3 +9,4 @@ package testing_go_code_with_postgres_test // in the following files: // - user_repository_with_isolated_database_test.go // - user_repository_with_transactional_cleanup_test.go +// - user_repository_with_isolated_schema_test.go diff --git a/user_repository_with_isolated_schema_test.go b/user_repository_with_isolated_schema_test.go new file mode 100644 index 0000000..9d68e11 --- /dev/null +++ b/user_repository_with_isolated_schema_test.go @@ -0,0 +1,134 @@ +// Copyright (c) 2023-2024 Vasiliy Vasilyuk. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testing_go_code_with_postgres_test + +import ( + "context" + "database/sql" + "errors" + "testing" + "time" + + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" + + "github.com/xorcare/testing-go-code-with-postgres/migrations" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + rootpkg "github.com/xorcare/testing-go-code-with-postgres" + "github.com/xorcare/testing-go-code-with-postgres/testingpg" +) + +func migrateDatabaseSchema(t *testing.T, pg *testingpg.Postgres) { + source, err := iofs.New(migrations.FS, ".") + require.NoError(t, err) + + mi, err := migrate.NewWithSourceInstance( + "iofs", + source, + pg.URL(), + ) + require.NoError(t, err) + + err = mi.Up() + + if !errors.Is(err, migrate.ErrNoChange) { + require.NoError(t, err) + } +} + +func Test_Schema_UserRepository_CreateUser(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + t.Parallel() + + newFullyFiledUser := func() rootpkg.User { + return rootpkg.User{ + ID: uuid.New(), + Username: "gopher", + CreatedAt: time.Now().Truncate(time.Microsecond), + } + } + + t.Run("Successfully created a User", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + t.Parallel() + + // Arrange + pg := testingpg.NewWithIsolatedSchema(t) + + migrateDatabaseSchema(t, pg) + + repo := rootpkg.NewUserRepository(pg.DB()) + user := newFullyFiledUser() + + // Act + err := repo.CreateUser(context.Background(), user) + + // Assert + require.NoError(t, err) + + gotUser, err := repo.ReadUser(context.Background(), user.ID) + require.NoError(t, err) + + require.Equal(t, user, gotUser) + }) + + t.Run("Cannot create a user with the same ID", func(t *testing.T) { + t.Parallel() + + // Arrange + pg := testingpg.NewWithIsolatedSchema(t) + + migrateDatabaseSchema(t, pg) + + repo := rootpkg.NewUserRepository(pg.DB()) + + user := newFullyFiledUser() + + err := repo.CreateUser(context.Background(), user) + require.NoError(t, err) + + // Act + err = repo.CreateUser(context.Background(), user) + + // Assert + require.Error(t, err) + require.Contains(t, err.Error(), "duplicate key value violates unique constraint") + }) +} + +func Test_Schema_UserRepository_ReadUser(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + t.Parallel() + + t.Run("Get an error if the user does not exist", func(t *testing.T) { + t.Parallel() + + // Arrange + pg := testingpg.NewWithIsolatedSchema(t) + + migrateDatabaseSchema(t, pg) + + repo := rootpkg.NewUserRepository(pg.DB()) + + // Act + _, err := repo.ReadUser(context.Background(), uuid.New()) + + // Assert + require.ErrorIs(t, err, sql.ErrNoRows) + }) +}