This repository has been archived by the owner on Jul 12, 2023. It is now read-only.
/
database_util.go
194 lines (170 loc) · 5.34 KB
/
database_util.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"testing"
"time"
"github.com/golang-migrate/migrate/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/ory/dockertest"
"github.com/sethvargo/go-retry"
// imported to register the postgres migration driver
_ "github.com/golang-migrate/migrate/v4/database/postgres"
// imported to register the "file" source migration driver
_ "github.com/golang-migrate/migrate/v4/source/file"
// imported to register the "postgres" database driver for migrate
)
// NewTestDatabaseWithConfig creates a new database suitable for use in testing.
// This should not be used outside of testing, but it is exposed in the main
// package so it can be shared with other packages.
//
// All database tests can be skipped by running `go test -short` or by setting
// the `SKIP_DATABASE_TESTS` environment variable.
func NewTestDatabaseWithConfig(tb testing.TB) (*DB, *Config) {
tb.Helper()
if testing.Short() {
tb.Skipf("🚧 Skipping database tests (short!")
}
if skip, _ := strconv.ParseBool(os.Getenv("SKIP_DATABASE_TESTS")); skip {
tb.Skipf("🚧 Skipping database tests (SKIP_DATABASE_TESTS is set)!")
}
// Context.
ctx := context.Background()
// Create the pool (docker instance).
pool, err := dockertest.NewPool("")
if err != nil {
tb.Fatalf("failed to create Docker pool: %s", err)
}
// Start the container.
dbname, username, password := "en-server", "my-username", "abcd1234"
container, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "postgres",
Tag: "12-alpine",
Env: []string{
"LANG=C",
"POSTGRES_DB=" + dbname,
"POSTGRES_USER=" + username,
"POSTGRES_PASSWORD=" + password,
},
})
if err != nil {
tb.Fatalf("failed to start postgres container: %s", err)
}
// Ensure container is cleaned up.
tb.Cleanup(func() {
if err := pool.Purge(container); err != nil {
tb.Fatalf("failed to cleanup postgres container: %s", err)
}
})
// Get the host. On Mac, Docker runs in a VM.
host := container.Container.NetworkSettings.IPAddress
if runtime.GOOS == "darwin" {
host = net.JoinHostPort(container.GetBoundIP("5432/tcp"), container.GetPort("5432/tcp"))
}
// Build the connection URL.
connURL := &url.URL{
Scheme: "postgres",
User: url.UserPassword(username, password),
Host: host,
Path: dbname,
}
q := connURL.Query()
q.Add("sslmode", "disable")
connURL.RawQuery = q.Encode()
// Wait for the container to start - we'll retry connections in a loop below,
// but there's no point in trying immediately.
time.Sleep(1 * time.Second)
b, err := retry.NewFibonacci(500 * time.Millisecond)
if err != nil {
tb.Fatalf("failed to configure backoff: %v", err)
}
b = retry.WithMaxRetries(10, b)
b = retry.WithCappedDuration(10*time.Second, b)
// Establish a connection to the database. Use a Fibonacci backoff instead of
// exponential so wait times scale appropriately.
var dbpool *pgxpool.Pool
if err := retry.Do(ctx, b, func(ctx context.Context) error {
var err error
dbpool, err = pgxpool.Connect(ctx, connURL.String())
if err != nil {
return retry.RetryableError(err)
}
return nil
}); err != nil {
tb.Fatalf("failed to start postgres: %s", err)
}
// Run the migrations.
if err := dbMigrate(connURL.String()); err != nil {
tb.Fatalf("failed to migrate database: %s", err)
}
// Create the db instance.
db := &DB{Pool: dbpool}
// Close db when done.
tb.Cleanup(func() {
db.Close(context.Background())
})
return db, &Config{
Name: dbname,
User: username,
Host: container.GetBoundIP("5432/tcp"),
Port: container.GetPort("5432/tcp"),
SSLMode: "disable",
Password: password,
}
}
func NewTestDatabase(tb testing.TB) *DB {
tb.Helper()
db, _ := NewTestDatabaseWithConfig(tb)
return db
}
// dbMigrate runs the migrations. u is the connection URL string (e.g.
// postgres://...).
func dbMigrate(u string) error {
// Run the migrations
migrationsDir := fmt.Sprintf("file://%s", dbMigrationsDir())
m, err := migrate.New(migrationsDir, u)
if err != nil {
return fmt.Errorf("failed create migrate: %w", err)
}
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("failed run migrate: %w", err)
}
srcErr, dbErr := m.Close()
if srcErr != nil {
return fmt.Errorf("migrate source error: %w", srcErr)
}
if dbErr != nil {
return fmt.Errorf("migrate database error: %w", dbErr)
}
return nil
}
// dbMigrationsDir returns the path on disk to the migrations. It uses
// runtime.Caller() to get the path to the caller, since this package is
// imported by multiple others at different levels.
func dbMigrationsDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
return ""
}
return filepath.Join(filepath.Dir(filename), "../../migrations")
}