Ctrl K
ComunidadGuías…

cli/provisionerjobs_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cli_test

import (
	"bytes"
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/cli/clitest"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbgen"
	"github.com/coder/coder/v2/coderd/database/dbtestutil"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/provisionersdk"
	"github.com/coder/coder/v2/testutil"
)

func TestProvisionerJobs(t *testing.T) {
	t.Parallel()

	t.Run("Cancel", func(t *testing.T) {
		t.Parallel()

		db, ps := dbtestutil.NewDB(t)
		client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{
			IncludeProvisionerDaemon: false,
			Database:                 db,
			Pubsub:                   ps,
		})
		owner := coderdtest.CreateFirstUser(t, client)
		templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
		memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// These CLI tests are related to provisioner job CRUD operations and as such
		// do not require the overhead of starting a provisioner. Other provisioner job
		// functionalities (acquisition etc.) are tested elsewhere.
		template := dbgen.Template(t, db, database.Template{
			OrganizationID:               owner.OrganizationID,
			CreatedBy:                    owner.UserID,
			AllowUserCancelWorkspaceJobs: true,
		})
		version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
			OrganizationID: owner.OrganizationID,
			CreatedBy:      owner.UserID,
			TemplateID:     uuid.NullUUID{UUID: template.ID, Valid: true},
		})
		// Test helper to create a provisioner job of a given type with a given input.
		prepareJob := func(t *testing.T, jobType database.ProvisionerJobType, input json.RawMessage) database.ProvisionerJob {
			t.Helper()
			return dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
				InitiatorID: member.ID,
				Input:       input,
				Type:        jobType,
				StartedAt:   sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Minute), Valid: true},
				Tags:        database.StringMap{provisionersdk.TagOwner: "", provisionersdk.TagScope: provisionersdk.ScopeOrganization, "foo": uuid.NewString()},
			})
		}

		// Test helper to create a workspace build job with a predefined input.
		prepareWorkspaceBuildJob := func(t *testing.T) database.ProvisionerJob {
			t.Helper()
			var (
				wbID     = uuid.New()
				input, _ = json.Marshal(map[string]string{"workspace_build_id": wbID.String()})
				job      = prepareJob(t, database.ProvisionerJobTypeWorkspaceBuild, input)
				w        = dbgen.Workspace(t, db, database.WorkspaceTable{
					OrganizationID: owner.OrganizationID,
					OwnerID:        member.ID,
					TemplateID:     template.ID,
				})
				_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
					ID:                wbID,
					InitiatorID:       member.ID,
					WorkspaceID:       w.ID,
					TemplateVersionID: version.ID,
					JobID:             job.ID,
				})
			)
			return job
		}

		// Test helper to create a template version import job with a predefined input.
		prepareTemplateVersionImportJob := func(t *testing.T) database.ProvisionerJob {
			t.Helper()
			var (
				tvID     = uuid.New()
				input, _ = json.Marshal(map[string]string{"template_version_id": tvID.String()})
				job      = prepareJob(t, database.ProvisionerJobTypeTemplateVersionImport, input)
				_        = dbgen.TemplateVersion(t, db, database.TemplateVersion{
					OrganizationID: owner.OrganizationID,
					CreatedBy:      templateAdmin.ID,
					ID:             tvID,
					TemplateID:     uuid.NullUUID{UUID: template.ID, Valid: true},
					JobID:          job.ID,
				})
			)
			return job
		}

		// Test helper to create a template version import dry run job with a predefined input.
		prepareTemplateVersionImportJobDryRun := func(t *testing.T) database.ProvisionerJob {
			t.Helper()
			var (
				tvID     = uuid.New()
				input, _ = json.Marshal(map[string]interface{}{
					"template_version_id": tvID.String(),
					"dry_run":             true,
				})
				job = prepareJob(t, database.ProvisionerJobTypeTemplateVersionDryRun, input)
				_   = dbgen.TemplateVersion(t, db, database.TemplateVersion{
					OrganizationID: owner.OrganizationID,
					CreatedBy:      templateAdmin.ID,
					ID:             tvID,
					TemplateID:     uuid.NullUUID{UUID: template.ID, Valid: true},
					JobID:          job.ID,
				})
			)
			return job
		}

		// Run the cancellation test suite.
		for _, tt := range []struct {
			role          string
			client        *codersdk.Client
			name          string
			prepare       func(*testing.T) database.ProvisionerJob
			wantCancelled bool
		}{
			{"Owner", client, "WorkspaceBuild", prepareWorkspaceBuildJob, true},
			{"Owner", client, "TemplateVersionImport", prepareTemplateVersionImportJob, true},
			{"Owner", client, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, true},
			{"TemplateAdmin", templateAdminClient, "WorkspaceBuild", prepareWorkspaceBuildJob, false},
			{"TemplateAdmin", templateAdminClient, "TemplateVersionImport", prepareTemplateVersionImportJob, true},
			{"TemplateAdmin", templateAdminClient, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, false},
			{"Member", memberClient, "WorkspaceBuild", prepareWorkspaceBuildJob, false},
			{"Member", memberClient, "TemplateVersionImport", prepareTemplateVersionImportJob, false},
			{"Member", memberClient, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, false},
		} {
			wantMsg := "OK"
			if !tt.wantCancelled {
				wantMsg = "FAIL"
			}
			t.Run(fmt.Sprintf("%s/%s/%v", tt.role, tt.name, wantMsg), func(t *testing.T) {
				t.Parallel()

				job := tt.prepare(t)
				require.False(t, job.CanceledAt.Valid, "job.CanceledAt.Valid")

				inv, root := clitest.New(t, "provisioner", "jobs", "cancel", job.ID.String())
				clitest.SetupConfig(t, tt.client, root)
				var buf bytes.Buffer
				inv.Stdout = &buf
				err := inv.Run()
				if tt.wantCancelled {
					assert.NoError(t, err)
				} else {
					assert.Error(t, err)
				}

				job, err = db.GetProvisionerJobByID(testutil.Context(t, testutil.WaitShort), job.ID)
				require.NoError(t, err)
				assert.Equal(t, tt.wantCancelled, job.CanceledAt.Valid, "job.CanceledAt.Valid")
				assert.Equal(t, tt.wantCancelled, job.CanceledAt.Time.After(job.StartedAt.Time), "job.CanceledAt.Time")
				if tt.wantCancelled {
					assert.Contains(t, buf.String(), "Job canceled")
				} else {
					assert.NotContains(t, buf.String(), "Job canceled")
				}
			})
		}
	})

	t.Run("List", func(t *testing.T) {
		t.Parallel()

		db, ps := dbtestutil.NewDB(t)
		client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{
			IncludeProvisionerDaemon: false,
			Database:                 db,
			Pubsub:                   ps,
		})
		owner := coderdtest.CreateFirstUser(t, client)
		_, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// These CLI tests are related to provisioner job CRUD operations and as such
		// do not require the overhead of starting a provisioner. Other provisioner job
		// functionalities (acquisition etc.) are tested elsewhere.
		template := dbgen.Template(t, db, database.Template{
			OrganizationID:               owner.OrganizationID,
			CreatedBy:                    owner.UserID,
			AllowUserCancelWorkspaceJobs: true,
		})
		version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
			OrganizationID: owner.OrganizationID,
			CreatedBy:      owner.UserID,
			TemplateID:     uuid.NullUUID{UUID: template.ID, Valid: true},
		})
		// Create some test jobs
		job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
			OrganizationID: owner.OrganizationID,
			InitiatorID:    owner.UserID,
			Type:           database.ProvisionerJobTypeTemplateVersionImport,
			Input:          []byte(`{"template_version_id":"` + version.ID.String() + `"}`),
			Tags:           database.StringMap{provisionersdk.TagScope: provisionersdk.ScopeOrganization},
		})

		job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
			OrganizationID: owner.OrganizationID,
			InitiatorID:    member.ID,
			Type:           database.ProvisionerJobTypeWorkspaceBuild,
			Input:          []byte(`{"workspace_build_id":"` + uuid.New().String() + `"}`),
			Tags:           database.StringMap{provisionersdk.TagScope: provisionersdk.ScopeOrganization},
		})
		// Test basic list command
		t.Run("Basic", func(t *testing.T) {
			t.Parallel()

			inv, root := clitest.New(t, "provisioner", "jobs", "list")
			clitest.SetupConfig(t, client, root)
			var buf bytes.Buffer
			inv.Stdout = &buf
			err := inv.Run()
			require.NoError(t, err)

			// Should contain both jobs
			output := buf.String()
			assert.Contains(t, output, job1.ID.String())
			assert.Contains(t, output, job2.ID.String())
		})

		// Test list with JSON output
		t.Run("JSON", func(t *testing.T) {
			t.Parallel()

			inv, root := clitest.New(t, "provisioner", "jobs", "list", "--output", "json")
			clitest.SetupConfig(t, client, root)
			var buf bytes.Buffer
			inv.Stdout = &buf
			err := inv.Run()
			require.NoError(t, err)

			// Parse JSON output
			var jobs []codersdk.ProvisionerJob
			err = json.Unmarshal(buf.Bytes(), &jobs)
			require.NoError(t, err)

			// Should contain both jobs
			jobIDs := make([]uuid.UUID, len(jobs))
			for i, job := range jobs {
				jobIDs[i] = job.ID
			}
			assert.Contains(t, jobIDs, job1.ID)
			assert.Contains(t, jobIDs, job2.ID)
		})

		// Test list with limit
		t.Run("Limit", func(t *testing.T) {
			t.Parallel()

			inv, root := clitest.New(t, "provisioner", "jobs", "list", "--limit", "1")
			clitest.SetupConfig(t, client, root)
			var buf bytes.Buffer
			inv.Stdout = &buf
			err := inv.Run()
			require.NoError(t, err)

			// Should contain at most 1 job
			output := buf.String()
			jobCount := 0
			if strings.Contains(output, job1.ID.String()) {
				jobCount++
			}
			if strings.Contains(output, job2.ID.String()) {
				jobCount++
			}
			assert.LessOrEqual(t, jobCount, 1)
		})

		// Test list with initiator filter
		t.Run("InitiatorFilter", func(t *testing.T) {
			t.Parallel()

			// Get owner user details to access username
			ctx := testutil.Context(t, testutil.WaitShort)
			ownerUser, err := client.User(ctx, owner.UserID.String())
			require.NoError(t, err)

			// Test filtering by initiator (using username)
			inv, root := clitest.New(t, "provisioner", "jobs", "list", "--initiator", ownerUser.Username)
			clitest.SetupConfig(t, client, root)
			var buf bytes.Buffer
			inv.Stdout = &buf
			err = inv.Run()
			require.NoError(t, err)

			// Should only contain job1 (initiated by owner)
			output := buf.String()
			assert.Contains(t, output, job1.ID.String())
			assert.NotContains(t, output, job2.ID.String())
		})

		// Test list with invalid user
		t.Run("InvalidUser", func(t *testing.T) {
			t.Parallel()

			// Test with non-existent user
			inv, root := clitest.New(t, "provisioner", "jobs", "list", "--initiator", "nonexistent-user")
			clitest.SetupConfig(t, client, root)
			var buf bytes.Buffer
			inv.Stdout = &buf
			err := inv.Run()
			require.Error(t, err)
			assert.Contains(t, err.Error(), "initiator not found: nonexistent-user")
		})
	})
}