Skip to content
Prev Previous commit
Next Next commit
Add unit test for statuses
  • Loading branch information
Emyrk committed May 30, 2023
commit 7d4b7eaec975306fc627d77ec251377e715b0e2e
11 changes: 11 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import (
"github.com/coder/coder/coderd/rbac"
)

// IsDBAuthzStore is provided for unit testing only.
type IsDBAuthzStore interface {
UnderlyingDatabase() database.Store
}

var _ database.Store = (*querier)(nil)

// NoActorError wraps ErrNoRows for the api to return a 404. This is the correct
Expand Down Expand Up @@ -99,6 +104,12 @@ func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger) data
}
}

// UnderlyingDatabase is dangerous and should not be used. This is only provided to accommodate some
// unit testing that has to manipulate the database directly.
func (q *querier) UnderlyingDatabase() database.Store {
return q.db
}

// authorizeContext is a helper function to authorize an action on an object.
func (q *querier) authorizeContext(ctx context.Context, action rbac.Action, object rbac.Objecter) error {
act, ok := ActorFromContext(ctx)
Expand Down
3 changes: 2 additions & 1 deletion coderd/database/dbauthz/system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dbauthz_test
import (
"context"
"database/sql"
"encoding/json"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -244,7 +245,7 @@ func (s *MethodTestSuite) TestSystemFunctions() {
j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{
StartedAt: sql.NullTime{Valid: false},
})
check.Args(database.AcquireProvisionerJobParams{Types: []database.ProvisionerType{j.Provisioner}}).
check.Args(database.AcquireProvisionerJobParams{Types: []database.ProvisionerType{j.Provisioner}, Tags: must(json.Marshal(j.Tags))}).
Asserts( /*rbac.ResourceSystem, rbac.ActionUpdate*/ )
}))
s.Run("UpdateProvisionerJobWithCompleteByID", s.Subtest(func(db database.Store, check *expects) {
Expand Down
29 changes: 19 additions & 10 deletions coderd/database/dbgen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,13 @@ func GroupMember(t testing.TB, db database.Store, orig database.GroupMember) dat
// ProvisionerJob might not have all the correct values like CompletedAt and CancelledAt. This is because
// the workspaceBuild is required to fetch those,
func ProvisionerJob(t testing.TB, db database.Store, orig database.ProvisionerJob) database.ProvisionerJob {
if dba, ok := db.(dbauthz.IsDBAuthzStore); ok {
db = dba.UnderlyingDatabase()
}

id := takeFirst(orig.ID, uuid.New())
if !orig.StartedAt.Time.IsZero() {
// Always set some tags to prevent Acquire from grabbing jobs it should not.
if !orig.StartedAt.Time.IsZero() || orig.Tags == nil {
if orig.Tags == nil {
orig.Tags = make(dbtype.StringMap)
}
Expand All @@ -300,10 +305,19 @@ func ProvisionerJob(t testing.TB, db database.Store, orig database.ProvisionerJo
})
require.NoError(t, err, "insert job")

if !orig.StartedAt.Time.IsZero() {
job, err = db.AcquireProvisionerJob(genCtx, database.AcquireProvisionerJobParams{
StartedAt: orig.StartedAt,
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: must(json.Marshal(orig.Tags)),
})
require.NoError(t, err)
}

if !orig.CompletedAt.Time.IsZero() || orig.Error.String != "" {
err := db.UpdateProvisionerJobWithCompleteByID(genCtx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: job.ID,
UpdatedAt: orig.UpdatedAt,
UpdatedAt: job.UpdatedAt,
CompletedAt: orig.CompletedAt,
Error: orig.Error,
ErrorCode: orig.ErrorCode,
Expand All @@ -318,14 +332,9 @@ func ProvisionerJob(t testing.TB, db database.Store, orig database.ProvisionerJo
})
require.NoError(t, err)
}
if !orig.StartedAt.Time.IsZero() {
job, err = db.AcquireProvisionerJob(genCtx, database.AcquireProvisionerJobParams{
StartedAt: orig.StartedAt,
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: must(json.Marshal(orig.Tags)),
})
require.NoError(t, err)
}

job, err = db.GetProvisionerJobByID(genCtx, job.ID)
require.NoError(t, err)

return job
}
Expand Down
84 changes: 71 additions & 13 deletions coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,38 +608,96 @@ func TestWorkspaceFilterAllStatus(t *testing.T) {
})

var err error
job, err = db.GetProvisionerJobByID(ctx, job.IDtus
)
job, err = db.GetProvisionerJobByID(ctx, job.ID)
require.NoError(t, err)

return workspace, build, job
}

// pending
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusPending),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Valid: false},
}, database.WorkspaceTransitionStart)

// starting
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusStarting),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
}, database.WorkspaceTransitionStart)

// running
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusRunning),
}, database.ProvisionerJob{
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
}, database.WorkspaceTransitionStart)

workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
require.NoError(t, err)

for _, apiWorkspace := range workspaces.Workspaces {
require.Equal(t, apiWorkspace.Name, string(apiWorkspace.LatestBuild.Status))
}

// pending
// starting
// running
// stopping
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusStopping),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
}, database.WorkspaceTransitionStop)

// stopped
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusStopped),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
}, database.WorkspaceTransitionStop)

// failed
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusFailed),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
Error: sql.NullString{String: "Some error", Valid: true},
}, database.WorkspaceTransitionStart)

// canceling
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusCanceling),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
CanceledAt: sql.NullTime{Time: time.Now(), Valid: true},
}, database.WorkspaceTransitionStart)

// canceled
// deleted
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusCanceled),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
CanceledAt: sql.NullTime{Time: time.Now(), Valid: true},
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
}, database.WorkspaceTransitionStart)

// deleting
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusDeleting),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
}, database.WorkspaceTransitionDelete)

// deleted
_, _, _ = makeWorkspace(database.Workspace{
Name: string(database.WorkspaceStatusDeleted),
}, database.ProvisionerJob{
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
}, database.WorkspaceTransitionDelete)
Copy link
Contributor

Choose a reason for hiding this comment

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

these tests are great and all, but they are not combinatorially exhaustive on null vs non-null for all fields. This is another reason I think a stored procedure for the status is better.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is definitely not exhaustive. The annoying thing too is when we prepulate this much data (which isn't even that much), the postgres test gets so slow. So I skipped it in CI, meaning this bug could easily pop up again silently 😢.

I was debating about putting in a batch insert SQL query outside our typical db interface and see if it is faster. If we go the stored procedure route, might be worth investigating how to make the postgres test run in a reasonable amount of time.

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if the slowness is related to how we have to create the job, then acquire it, then fail/complete it. Acquiring in particular might be slow because of the locking we do.

In testing we could have some queries that directly insert the job in the state we want it. Not exposed on the main Store interface, but available if you type assert to a test type.


workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
require.NoError(t, err)

for _, apiWorkspace := range workspaces.Workspaces {
require.Equal(t, apiWorkspace.Name, string(apiWorkspace.LatestBuild.Status))
}
}

// TestWorkspaceFilter creates a set of workspaces, users, and organizations
Expand Down