package adapter_test import ( "context" "database/sql" "errors" "fmt" "net" "net/url" "os" "strings" "sync" "testing" "time" "unicode" "gitea.dwysokinski.me/twhelp/dcbot/internal/adapter/internal/bunmodel" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "github.com/cenkalti/backoff/v4" "github.com/google/uuid" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "github.com/pressly/goose/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uptrace/bun" "github.com/uptrace/bun/dbfixture" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" ) var ( gooseSetUpOnce = sync.Once{} ) func newBunDB(tb testing.TB) *bun.DB { tb.Helper() if dsn, ok := os.LookupEnv("TESTS_DB_DSN"); ok { return newBunDBWithDSN(tb, dsn) } q := url.Values{} q.Add("sslmode", "disable") dsn := &url.URL{ Scheme: "postgres", User: url.UserPassword("postgres", "postgres"), Path: "twhelp", RawQuery: q.Encode(), } pool, err := dockertest.NewPool("") require.NoError(tb, err, "couldn't connect to docker") pool.MaxWait = 20 * time.Second pw, _ := dsn.User.Password() resource, err := pool.RunWithOptions(&dockertest.RunOptions{ Repository: "postgres", Tag: "14.8", Env: []string{ fmt.Sprintf("POSTGRES_USER=%s", dsn.User.Username()), fmt.Sprintf("POSTGRES_PASSWORD=%s", pw), fmt.Sprintf("POSTGRES_DB=%s", dsn.Path), }, }, func(config *docker.HostConfig) { config.AutoRemove = true config.RestartPolicy = docker.RestartPolicy{ Name: "no", } }) require.NoError(tb, err, "couldn't start resource") tb.Cleanup(func() { _ = pool.Purge(resource) }) assert.NoError(tb, resource.Expire(60)) dsn.Host = getHostPort(tb, resource, "5432/tcp") return newBunDBWithDSN(tb, dsn.String()) } func newBunDBWithDSN(tb testing.TB, dsn string) *bun.DB { tb.Helper() schema := generateSchema() sqldb := sql.OpenDB( pgdriver.NewConnector( pgdriver.WithDSN(dsn), pgdriver.WithConnParams(map[string]interface{}{ "search_path": schema, }), ), ) bunDB := bun.NewDB(sqldb, pgdialect.New()) tb.Cleanup(func() { _ = bunDB.Close() }) require.NoError(tb, retry(bunDB.Ping), "couldn't ping DB") _, err := bunDB.Exec("CREATE SCHEMA ?", bun.Safe(schema)) require.NoError(tb, err, "couldn't create schema") runMigrations(tb, sqldb) return bunDB } func getHostPort(tb testing.TB, resource *dockertest.Resource, id string) string { tb.Helper() dockerURL := os.Getenv("DOCKER_HOST") if dockerURL == "" { return resource.GetHostPort(id) } u, err := url.Parse(dockerURL) require.NoError(tb, err) return net.JoinHostPort(u.Hostname(), resource.GetPort(id)) } func runMigrations(tb testing.TB, db *sql.DB) { tb.Helper() gooseSetUpOnce.Do(func() { goose.SetBaseFS(os.DirFS("../../migrations")) goose.SetLogger(goose.NopLogger()) }) require.NoError(tb, goose.Up(db, "")) } type bunfixture struct { *dbfixture.Fixture } func loadFixtures(tb testing.TB, bunDB *bun.DB) *bunfixture { tb.Helper() // ensure that models are registered bunDB.RegisterModel(&bunmodel.Group{}, &bunmodel.Monitor{}) fixture := dbfixture.New(bunDB) err := fixture.Load(context.Background(), os.DirFS("testdata"), "fixture.yml") require.NoError(tb, err, "couldn't load fixtures") return &bunfixture{fixture} } func (f *bunfixture) group(tb testing.TB, id string) domain.GroupWithMonitors { tb.Helper() row, err := f.Row("Group." + id) require.NoError(tb, err) g, ok := row.(*bunmodel.Group) require.True(tb, ok) return g.ToDomain() } func (f *bunfixture) groups(tb testing.TB) []domain.GroupWithMonitors { tb.Helper() //nolint:lll ids := []string{"group-1-server-1", "group-2-server-1", "group-1-server-2", "group-2-server-2", "group-3-server-1", "group-3-server-2"} groups := make([]domain.GroupWithMonitors, 0, len(ids)) for _, id := range ids { groups = append(groups, f.group(tb, id)) } return groups } func (f *bunfixture) monitor(tb testing.TB, id string) domain.Monitor { tb.Helper() row, err := f.Row("Monitor." + id) require.NoError(tb, err) m, ok := row.(*bunmodel.Monitor) require.True(tb, ok) return m.ToDomain() } func generateSchema() string { return strings.TrimFunc(strings.ReplaceAll(uuid.NewString(), "-", "_"), unicode.IsNumber) } func retry(op func() error) error { bo := backoff.NewExponentialBackOff() bo.MaxInterval = time.Second * 5 bo.MaxElapsedTime = 30 * time.Second if err := backoff.Retry(op, bo); err != nil { if bo.NextBackOff() == backoff.Stop { return errors.New("reached retry deadline") } return err } return nil }