Skip to content

feat(coderd/database/dbtestutil): set default database timezone to non-UTC in unit tests #9672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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 cli/clitest/golden.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
// make update-golden-files
var UpdateGoldenFiles = flag.Bool("update", false, "update .golden files")

var timestampRegex = regexp.MustCompile(`(?i)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?Z`)
var timestampRegex = regexp.MustCompile(`(?i)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?(Z|[+-]\d+:\d+)`)

type CommandHelpCase struct {
Name string
Expand Down
2 changes: 2 additions & 0 deletions coderd/activitybump_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestWorkspaceActivityBump(t *testing.T) {
// doesn't use template autostop requirements and instead edits the
// max_deadline on the build directly in the database.
setupActivityTest := func(t *testing.T, deadline ...time.Duration) (client *codersdk.Client, workspace codersdk.Workspace, assertBumped func(want bool)) {
t.Helper()
const ttl = time.Minute
maxTTL := time.Duration(0)
if len(deadline) > 0 {
Expand Down Expand Up @@ -120,6 +121,7 @@ func TestWorkspaceActivityBump(t *testing.T) {
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)

return client, workspace, func(want bool) {
t.Helper()
if !want {
// It is difficult to test the absence of a call in a non-racey
// way. In general, it is difficult for the API to generate
Expand Down
61 changes: 60 additions & 1 deletion coderd/database/dbtestutil/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package dbtestutil
import (
"context"
"database/sql"
"fmt"
"net/url"
"os"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbtestutil/randtz"
"github.com/coder/coder/v2/coderd/database/postgres"
"github.com/coder/coder/v2/coderd/database/pubsub"
)
Expand All @@ -19,9 +23,30 @@ func WillUsePostgres() bool {
return os.Getenv("DB") != ""
}

func NewDB(t testing.TB) (database.Store, pubsub.Pubsub) {
type options struct {
fixedTimezone string
}

type Option func(*options)

// WithTimezone sets the database to the defined timezone instead of
// to a random one.
//
// Deprecated: If you need to use this, you may have a timezone-related bug.
func WithTimezone(tz string) Option {
return func(o *options) {
o.fixedTimezone = tz
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review: I originally exposed this as WithFixedTimezone() but I quickly realized that it's useful to be able to set a specific timezone in unit tests, especially when you're trying to narrow down the root cause of a timezone-related bug.


func NewDB(t testing.TB, opts ...Option) (database.Store, pubsub.Pubsub) {
t.Helper()

var o options
for _, opt := range opts {
opt(&o)
}

db := dbfake.New()
ps := pubsub.NewInMemory()
if WillUsePostgres() {
Expand All @@ -35,6 +60,15 @@ func NewDB(t testing.TB) (database.Store, pubsub.Pubsub) {
require.NoError(t, err)
t.Cleanup(closePg)
}

if o.fixedTimezone == "" {
// To make sure we find timezone-related issues, we set the timezone
// of the database to a random one.
o.fixedTimezone = randtz.Name(t)
}
dbName := dbNameFromConnectionURL(t, connectionURL)
setDBTimezone(t, connectionURL, dbName, o.fixedTimezone)

sqlDB, err := sql.Open("postgres", connectionURL)
require.NoError(t, err)
t.Cleanup(func() {
Expand All @@ -51,3 +85,28 @@ func NewDB(t testing.TB) (database.Store, pubsub.Pubsub) {

return db, ps
}

// setRandDBTimezone sets the timezone of the database to the given timezone.
func setDBTimezone(t testing.TB, dbURL, dbname, tz string) {
t.Helper()

sqlDB, err := sql.Open("postgres", dbURL)
require.NoError(t, err)
defer func() {
// The updated timezone only comes into effect on reconnect, so close
// the DB after we're done.
_ = sqlDB.Close()
}()

// nolint: gosec // This unfortunately does not work with placeholders.
_, err = sqlDB.Exec(fmt.Sprintf("ALTER DATABASE %s SET TIMEZONE TO %q", dbname, tz))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self-review: I hate this but it appears necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine in tests, IMO. It's an unfortunate limitation of some PostgreSQL queries.

require.NoError(t, err, "failed to set timezone for database")
}

// dbNameFromConnectionURL returns the database name from the given connection URL,
// where connectionURL is of the form postgres://user:pass@host:port/dbname
func dbNameFromConnectionURL(t testing.TB, connectionURL string) string {
u, err := url.Parse(connectionURL)
require.NoError(t, err)
return strings.TrimPrefix(u.Path, "/")
}
Loading