coder/coderd/provisionerjobs_internal_te...

292 lines
7.2 KiB
Go

package coderd
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/json"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)
func TestProvisionerJobLogs_Unit(t *testing.T) {
t.Parallel()
t.Run("QueryPubSubDupes", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
// mDB := mocks.NewStore(t)
fDB := databasefake.New()
fPubsub := &fakePubSub{t: t, cond: sync.NewCond(&sync.Mutex{})}
opts := Options{
Logger: logger,
Database: fDB,
Pubsub: fPubsub,
}
api := New(&opts)
server := httptest.NewServer(api.Handler)
defer server.Close()
userID := uuid.New()
keyID, keySecret, err := generateAPIKeyIDSecret()
require.NoError(t, err)
hashed := sha256.Sum256([]byte(keySecret))
u, err := url.Parse(server.URL)
require.NoError(t, err)
client := codersdk.Client{
HTTPClient: server.Client(),
SessionToken: keyID + "-" + keySecret,
URL: u,
}
buildID := uuid.New()
workspaceID := uuid.New()
jobID := uuid.New()
expectedLogs := []database.ProvisionerJobLog{
{ID: uuid.New(), JobID: jobID, Stage: "Stage0"},
{ID: uuid.New(), JobID: jobID, Stage: "Stage1"},
{ID: uuid.New(), JobID: jobID, Stage: "Stage2"},
{ID: uuid.New(), JobID: jobID, Stage: "Stage3"},
}
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
// wow there are a lot of DB rows we touch...
_, err = fDB.InsertAPIKey(ctx, database.InsertAPIKeyParams{
ID: keyID,
HashedSecret: hashed[:],
UserID: userID,
ExpiresAt: time.Now().Add(5 * time.Hour),
LoginType: database.LoginTypePassword,
})
require.NoError(t, err)
_, err = fDB.InsertUser(ctx, database.InsertUserParams{
ID: userID,
RBACRoles: []string{rbac.RoleOwner()},
})
require.NoError(t, err)
_, err = fDB.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
ID: buildID,
WorkspaceID: workspaceID,
JobID: jobID,
})
require.NoError(t, err)
_, err = fDB.InsertWorkspace(ctx, database.InsertWorkspaceParams{
ID: workspaceID,
})
require.NoError(t, err)
_, err = fDB.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: jobID,
})
require.NoError(t, err)
for _, l := range expectedLogs[:2] {
_, err := fDB.InsertProvisionerJobLogs(ctx, database.InsertProvisionerJobLogsParams{
ID: []uuid.UUID{l.ID},
JobID: jobID,
Stage: []string{l.Stage},
})
require.NoError(t, err)
}
logs, err := client.WorkspaceBuildLogsAfter(ctx, buildID, time.Now())
require.NoError(t, err)
// when the endpoint calls subscribe, we get the listener here.
fPubsub.cond.L.Lock()
for fPubsub.listener == nil {
fPubsub.cond.Wait()
}
// endpoint should now be listening
assert.False(t, fPubsub.canceled)
assert.False(t, fPubsub.closed)
// send all the logs in two batches, duplicating what we already returned on the DB query.
msg := provisionerJobLogsMessage{}
msg.Logs = expectedLogs[:2]
data, err := json.Marshal(msg)
require.NoError(t, err)
fPubsub.listener(ctx, data)
msg.Logs = expectedLogs[2:]
data, err = json.Marshal(msg)
require.NoError(t, err)
fPubsub.listener(ctx, data)
// send end of logs
msg.Logs = nil
msg.EndOfLogs = true
data, err = json.Marshal(msg)
require.NoError(t, err)
fPubsub.listener(ctx, data)
var stages []string
for l := range logs {
logger.Info(ctx, "got log",
slog.F("id", l.ID),
slog.F("stage", l.Stage))
stages = append(stages, l.Stage)
}
assert.Equal(t, []string{"Stage0", "Stage1", "Stage2", "Stage3"}, stages)
for !fPubsub.canceled {
fPubsub.cond.Wait()
}
assert.False(t, fPubsub.closed)
})
}
func TestConvertProvisionerJob_Unit(t *testing.T) {
t.Parallel()
validNullTimeMock := sql.NullTime{
Time: database.Now(),
Valid: true,
}
invalidNullTimeMock := sql.NullTime{}
errorMock := sql.NullString{
String: "error",
Valid: true,
}
testCases := []struct {
name string
input database.ProvisionerJob
expected codersdk.ProvisionerJob
}{
{
name: "empty",
input: database.ProvisionerJob{},
expected: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobPending,
},
},
{
name: "cancellation pending",
input: database.ProvisionerJob{
CanceledAt: validNullTimeMock,
CompletedAt: invalidNullTimeMock,
},
expected: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobCanceling,
},
},
{
name: "cancellation failed",
input: database.ProvisionerJob{
CanceledAt: validNullTimeMock,
CompletedAt: validNullTimeMock,
Error: errorMock,
},
expected: codersdk.ProvisionerJob{
CompletedAt: &validNullTimeMock.Time,
Status: codersdk.ProvisionerJobFailed,
Error: errorMock.String,
},
},
{
name: "cancellation succeeded",
input: database.ProvisionerJob{
CanceledAt: validNullTimeMock,
CompletedAt: validNullTimeMock,
},
expected: codersdk.ProvisionerJob{
CompletedAt: &validNullTimeMock.Time,
Status: codersdk.ProvisionerJobCanceled,
},
},
{
name: "job pending",
input: database.ProvisionerJob{
StartedAt: invalidNullTimeMock,
},
expected: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobPending,
},
},
{
name: "job failed",
input: database.ProvisionerJob{
CompletedAt: validNullTimeMock,
StartedAt: validNullTimeMock,
Error: errorMock,
},
expected: codersdk.ProvisionerJob{
CompletedAt: &validNullTimeMock.Time,
StartedAt: &validNullTimeMock.Time,
Error: errorMock.String,
Status: codersdk.ProvisionerJobFailed,
},
},
{
name: "job succeeded",
input: database.ProvisionerJob{
CompletedAt: validNullTimeMock,
StartedAt: validNullTimeMock,
},
expected: codersdk.ProvisionerJob{
CompletedAt: &validNullTimeMock.Time,
StartedAt: &validNullTimeMock.Time,
Status: codersdk.ProvisionerJobSucceeded,
},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
actual := convertProvisionerJob(testCase.input)
assert.Equal(t, testCase.expected, actual)
})
}
}
type fakePubSub struct {
t *testing.T
cond *sync.Cond
listener database.Listener
canceled bool
closed bool
}
func (f *fakePubSub) Subscribe(_ string, listener database.Listener) (cancel func(), err error) {
f.cond.L.Lock()
defer f.cond.L.Unlock()
f.listener = listener
f.cond.Signal()
return f.cancel, nil
}
func (f *fakePubSub) Publish(_ string, _ []byte) error {
f.t.Fail()
return nil
}
func (f *fakePubSub) Close() error {
f.cond.L.Lock()
defer f.cond.L.Unlock()
f.closed = true
f.cond.Signal()
return nil
}
func (f *fakePubSub) cancel() {
f.cond.L.Lock()
defer f.cond.L.Unlock()
f.canceled = true
f.cond.Signal()
}