diff --git a/e2e/console/main_test.go b/e2e/console/main_test.go index 8c86d8271..c874acad7 100644 --- a/e2e/console/main_test.go +++ b/e2e/console/main_test.go @@ -12,7 +12,6 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -// Package console_test contains end-to-end tests for the Console GraphQL API. package console_test import ( diff --git a/e2e/internal/factory/factory.go b/e2e/internal/factory/factory.go index 872ed11fc..f47c3052d 100644 --- a/e2e/internal/factory/factory.go +++ b/e2e/internal/factory/factory.go @@ -24,20 +24,16 @@ import ( "go.probo.inc/probo/e2e/internal/testutil" ) -// SafeName generates a name without special characters that might cause validation issues. func SafeName(prefix string) string { return fmt.Sprintf("%s %s", prefix, gofakeit.LetterN(8)) } -// SafeEmail generates an email using example.com domain to avoid validation issues. func SafeEmail() string { return fmt.Sprintf("%s@example.com", strings.ToLower(gofakeit.LetterN(12))) } -// Attrs is a map of attribute overrides for factory functions. type Attrs map[string]any -// get returns the value for key if present, otherwise returns defaultVal. func (a Attrs) get(key string, defaultVal any) any { if a == nil { return defaultVal @@ -48,7 +44,6 @@ func (a Attrs) get(key string, defaultVal any) any { return defaultVal } -// getString returns the string value for key if present, otherwise returns defaultVal. func (a Attrs) getString(key string, defaultVal string) string { if v, ok := a.get(key, defaultVal).(string); ok { return v @@ -56,7 +51,6 @@ func (a Attrs) getString(key string, defaultVal string) string { return defaultVal } -// getStringPtr returns a pointer to the string value for key if present. func (a Attrs) getStringPtr(key string) *string { if a == nil { return nil @@ -69,7 +63,6 @@ func (a Attrs) getStringPtr(key string) *string { return nil } -// getInt returns the int value for key if present, otherwise returns defaultVal. func (a Attrs) getInt(key string, defaultVal int) int { if a == nil { return defaultVal @@ -99,7 +92,6 @@ func (a Attrs) getBool(key string, defaultVal bool) bool { return defaultVal } -// CreateVendor creates a vendor with optional attribute overrides. func CreateVendor(c *testutil.Client, attrs ...Attrs) string { c.T.Helper() @@ -148,7 +140,6 @@ func CreateVendor(c *testutil.Client, attrs ...Attrs) string { return result.CreateVendor.VendorEdge.Node.ID } -// CreateFramework creates a framework with optional attribute overrides. func CreateFramework(c *testutil.Client, attrs ...Attrs) string { c.T.Helper() @@ -191,8 +182,6 @@ func CreateFramework(c *testutil.Client, attrs ...Attrs) string { return result.CreateFramework.FrameworkEdge.Node.ID } -// CreateControl creates a control with optional attribute overrides. -// The frameworkId is required and must be passed in attrs. func CreateControl(c *testutil.Client, frameworkID string, attrs ...Attrs) string { c.T.Helper() @@ -235,7 +224,6 @@ func CreateControl(c *testutil.Client, frameworkID string, attrs ...Attrs) strin return result.CreateControl.ControlEdge.Node.ID } -// CreateMeasure creates a measure with optional attribute overrides. func CreateMeasure(c *testutil.Client, attrs ...Attrs) string { c.T.Helper() @@ -279,8 +267,6 @@ func CreateMeasure(c *testutil.Client, attrs ...Attrs) string { return result.CreateMeasure.MeasureEdge.Node.ID } -// CreateTask creates a task with optional attribute overrides. -// The measureId is required. func CreateTask(c *testutil.Client, measureID string, attrs ...Attrs) string { c.T.Helper() @@ -324,7 +310,6 @@ func CreateTask(c *testutil.Client, measureID string, attrs ...Attrs) string { return result.CreateTask.TaskEdge.Node.ID } -// CreateRisk creates a risk with optional attribute overrides. func CreateRisk(c *testutil.Client, attrs ...Attrs) string { c.T.Helper() @@ -371,7 +356,6 @@ func CreateRisk(c *testutil.Client, attrs ...Attrs) string { return result.CreateRisk.RiskEdge.Node.ID } -// CreatePeople creates a people record with optional attribute overrides. func CreatePeople(c *testutil.Client, attrs ...Attrs) string { c.T.Helper() @@ -413,18 +397,11 @@ func CreatePeople(c *testutil.Client, attrs ...Attrs) string { return result.CreatePeople.PeopleEdge.Node.ID } -// ============================================================================= -// Builder-style wrappers for backward compatibility -// These provide the fluent builder API that wraps the simpler factory functions -// ============================================================================= - -// VendorBuilder provides a fluent API for creating vendors. type VendorBuilder struct { client *testutil.Client attrs Attrs } -// NewVendor creates a new VendorBuilder. func NewVendor(c *testutil.Client) *VendorBuilder { return &VendorBuilder{client: c, attrs: Attrs{}} } @@ -453,13 +430,11 @@ func (b *VendorBuilder) Create() string { return CreateVendor(b.client, b.attrs) } -// FrameworkBuilder provides a fluent API for creating frameworks. type FrameworkBuilder struct { client *testutil.Client attrs Attrs } -// NewFramework creates a new FrameworkBuilder. func NewFramework(c *testutil.Client) *FrameworkBuilder { return &FrameworkBuilder{client: c, attrs: Attrs{}} } @@ -478,14 +453,12 @@ func (b *FrameworkBuilder) Create() string { return CreateFramework(b.client, b.attrs) } -// ControlBuilder provides a fluent API for creating controls. type ControlBuilder struct { client *testutil.Client frameworkID string attrs Attrs } -// NewControl creates a new ControlBuilder. func NewControl(c *testutil.Client, frameworkID string) *ControlBuilder { return &ControlBuilder{client: c, frameworkID: frameworkID, attrs: Attrs{}} } @@ -514,13 +487,11 @@ func (b *ControlBuilder) Create() string { return CreateControl(b.client, b.frameworkID, b.attrs) } -// MeasureBuilder provides a fluent API for creating measures. type MeasureBuilder struct { client *testutil.Client attrs Attrs } -// NewMeasure creates a new MeasureBuilder. func NewMeasure(c *testutil.Client) *MeasureBuilder { return &MeasureBuilder{client: c, attrs: Attrs{}} } @@ -544,14 +515,12 @@ func (b *MeasureBuilder) Create() string { return CreateMeasure(b.client, b.attrs) } -// TaskBuilder provides a fluent API for creating tasks. type TaskBuilder struct { client *testutil.Client measureID string attrs Attrs } -// NewTask creates a new TaskBuilder. func NewTask(c *testutil.Client, measureID string) *TaskBuilder { return &TaskBuilder{client: c, measureID: measureID, attrs: Attrs{}} } @@ -570,13 +539,11 @@ func (b *TaskBuilder) Create() string { return CreateTask(b.client, b.measureID, b.attrs) } -// RiskBuilder provides a fluent API for creating risks. type RiskBuilder struct { client *testutil.Client attrs Attrs } -// NewRisk creates a new RiskBuilder. func NewRisk(c *testutil.Client) *RiskBuilder { return &RiskBuilder{client: c, attrs: Attrs{}} } @@ -615,13 +582,11 @@ func (b *RiskBuilder) Create() string { return CreateRisk(b.client, b.attrs) } -// PeopleBuilder provides a fluent API for creating people. type PeopleBuilder struct { client *testutil.Client attrs Attrs } -// NewPeople creates a new PeopleBuilder. func NewPeople(c *testutil.Client) *PeopleBuilder { return &PeopleBuilder{client: c, attrs: Attrs{}} } @@ -640,8 +605,6 @@ func (b *PeopleBuilder) Create() string { return CreatePeople(b.client, b.attrs) } -// CreateAudit creates an audit with optional attribute overrides. -// The frameworkId is required. func CreateAudit(c *testutil.Client, frameworkID string, attrs ...Attrs) string { c.T.Helper() @@ -685,14 +648,12 @@ func CreateAudit(c *testutil.Client, frameworkID string, attrs ...Attrs) string return result.CreateAudit.AuditEdge.Node.ID } -// AuditBuilder provides a fluent API for creating audits. type AuditBuilder struct { client *testutil.Client frameworkID string attrs Attrs } -// NewAudit creates a new AuditBuilder. func NewAudit(c *testutil.Client, frameworkID string) *AuditBuilder { return &AuditBuilder{client: c, frameworkID: frameworkID, attrs: Attrs{}} } @@ -711,8 +672,6 @@ func (b *AuditBuilder) Create() string { return CreateAudit(b.client, b.frameworkID, b.attrs) } -// CreateDatum creates a datum with optional attribute overrides. -// The ownerId (peopleId) is required. func CreateDatum(c *testutil.Client, ownerID string, attrs ...Attrs) string { c.T.Helper() @@ -757,14 +716,12 @@ func CreateDatum(c *testutil.Client, ownerID string, attrs ...Attrs) string { return result.CreateDatum.DatumEdge.Node.ID } -// DatumBuilder provides a fluent API for creating data. type DatumBuilder struct { client *testutil.Client ownerID string attrs Attrs } -// NewDatum creates a new DatumBuilder. func NewDatum(c *testutil.Client, ownerID string) *DatumBuilder { return &DatumBuilder{client: c, ownerID: ownerID, attrs: Attrs{}} } @@ -788,7 +745,6 @@ func (b *DatumBuilder) Create() string { return CreateDatum(b.client, b.ownerID, b.attrs) } -// CreateMeeting creates a meeting with optional attribute overrides. func CreateMeeting(c *testutil.Client, attrs ...Attrs) string { c.T.Helper() @@ -832,13 +788,11 @@ func CreateMeeting(c *testutil.Client, attrs ...Attrs) string { return result.CreateMeeting.MeetingEdge.Node.ID } -// MeetingBuilder provides a fluent API for creating meetings. type MeetingBuilder struct { client *testutil.Client attrs Attrs } -// NewMeeting creates a new MeetingBuilder. func NewMeeting(c *testutil.Client) *MeetingBuilder { return &MeetingBuilder{client: c, attrs: Attrs{}} } @@ -862,8 +816,6 @@ func (b *MeetingBuilder) Create() string { return CreateMeeting(b.client, b.attrs) } -// CreateDocument creates a document with optional attribute overrides. -// The ownerId (peopleId) is required. func CreateDocument(c *testutil.Client, ownerID string, attrs ...Attrs) string { c.T.Helper() @@ -907,14 +859,12 @@ func CreateDocument(c *testutil.Client, ownerID string, attrs ...Attrs) string { return result.CreateDocument.DocumentEdge.Node.ID } -// DocumentBuilder provides a fluent API for creating documents. type DocumentBuilder struct { client *testutil.Client ownerID string attrs Attrs } -// NewDocument creates a new DocumentBuilder. func NewDocument(c *testutil.Client, ownerID string) *DocumentBuilder { return &DocumentBuilder{client: c, ownerID: ownerID, attrs: Attrs{}} } @@ -943,7 +893,6 @@ func (b *DocumentBuilder) Create() string { return CreateDocument(b.client, b.ownerID, b.attrs) } -// CreateProcessingActivity creates a processing activity with optional attribute overrides. func CreateProcessingActivity(c *testutil.Client, attrs ...Attrs) string { c.T.Helper() @@ -991,13 +940,11 @@ func CreateProcessingActivity(c *testutil.Client, attrs ...Attrs) string { return result.CreateProcessingActivity.ProcessingActivityEdge.Node.ID } -// ProcessingActivityBuilder provides a fluent API for creating processing activities. type ProcessingActivityBuilder struct { client *testutil.Client attrs Attrs } -// NewProcessingActivity creates a new ProcessingActivityBuilder. func NewProcessingActivity(c *testutil.Client) *ProcessingActivityBuilder { return &ProcessingActivityBuilder{client: c, attrs: Attrs{}} } diff --git a/e2e/internal/testutil/assert.go b/e2e/internal/testutil/assert.go index 5004b01b1..4a8ccb2cf 100644 --- a/e2e/internal/testutil/assert.go +++ b/e2e/internal/testutil/assert.go @@ -24,8 +24,6 @@ import ( "github.com/stretchr/testify/require" ) -// PageInfo represents GraphQL Relay-style pagination info. -// This type can be embedded in result structs to avoid repeating the definition. type PageInfo struct { HasNextPage bool `json:"hasNextPage"` HasPreviousPage bool `json:"hasPreviousPage"` @@ -33,9 +31,6 @@ type PageInfo struct { EndCursor *string `json:"endCursor"` } -// AssertFirstPage asserts that the response represents a valid first page of results. -// It checks that the edge count matches expected, hasNextPage equals expectMore, -// and hasPreviousPage is false. func AssertFirstPage(t *testing.T, edgeCount int, pageInfo PageInfo, expectedCount int, expectMore bool) { t.Helper() assert.Equal(t, expectedCount, edgeCount, "unexpected number of edges") @@ -43,8 +38,6 @@ func AssertFirstPage(t *testing.T, edgeCount int, pageInfo PageInfo, expectedCou assert.False(t, pageInfo.HasPreviousPage, "first page should not have previous page") } -// AssertMiddlePage asserts that the response represents a valid middle page of results. -// It checks that the edge count matches expected and both hasNextPage and hasPreviousPage are true. func AssertMiddlePage(t *testing.T, edgeCount int, pageInfo PageInfo, expectedCount int) { t.Helper() assert.Equal(t, expectedCount, edgeCount, "unexpected number of edges") @@ -52,9 +45,6 @@ func AssertMiddlePage(t *testing.T, edgeCount int, pageInfo PageInfo, expectedCo assert.True(t, pageInfo.HasPreviousPage, "middle page should have previous page") } -// AssertLastPage asserts that the response represents a valid last page of results. -// It checks that the edge count matches expected, hasNextPage is false, -// and hasPreviousPage equals expectPrevious. func AssertLastPage(t *testing.T, edgeCount int, pageInfo PageInfo, expectedCount int, expectPrevious bool) { t.Helper() assert.Equal(t, expectedCount, edgeCount, "unexpected number of edges") @@ -62,22 +52,18 @@ func AssertLastPage(t *testing.T, edgeCount int, pageInfo PageInfo, expectedCoun assert.Equal(t, expectPrevious, pageInfo.HasPreviousPage, "hasPreviousPage mismatch") } -// AssertHasMorePages asserts that there are more pages available after the current one. func AssertHasMorePages(t *testing.T, pageInfo PageInfo) { t.Helper() assert.True(t, pageInfo.HasNextPage, "expected more pages") assert.NotNil(t, pageInfo.EndCursor, "endCursor should be set when there are more pages") } -// AssertHasPreviousPages asserts that there are previous pages before the current one. func AssertHasPreviousPages(t *testing.T, pageInfo PageInfo) { t.Helper() assert.True(t, pageInfo.HasPreviousPage, "expected previous pages") assert.NotNil(t, pageInfo.StartCursor, "startCursor should be set when there are previous pages") } -// AssertTimestampsOnCreate validates that createdAt and updatedAt are properly set -// after resource creation: both should be after beforeCreate and equal to each other. func AssertTimestampsOnCreate(t *testing.T, createdAt, updatedAt, beforeCreate time.Time) { t.Helper() assert.True(t, createdAt.After(beforeCreate), "createdAt should be after test start") @@ -85,8 +71,6 @@ func AssertTimestampsOnCreate(t *testing.T, createdAt, updatedAt, beforeCreate t assert.Equal(t, createdAt, updatedAt, "createdAt and updatedAt should be equal on create") } -// AssertTimestampsOnUpdate validates that timestamps are properly updated: -// createdAt should remain unchanged, updatedAt should be after the previous value. func AssertTimestampsOnUpdate(t *testing.T, createdAt, updatedAt, originalCreatedAt, originalUpdatedAt time.Time) { t.Helper() assert.Equal(t, originalCreatedAt, createdAt, "createdAt should not change on update") @@ -94,8 +78,6 @@ func AssertTimestampsOnUpdate(t *testing.T, createdAt, updatedAt, originalCreate "updatedAt should be >= previous updatedAt") } -// AssertOptionalStringEqual validates optional string fields (pointers). -// If expected is nil, actual must be nil. Otherwise, both must be non-nil and equal. func AssertOptionalStringEqual(t *testing.T, expected, actual *string, fieldName string) { t.Helper() if expected == nil { @@ -106,13 +88,11 @@ func AssertOptionalStringEqual(t *testing.T, expected, actual *string, fieldName } } -// AssertOrderedAscending validates that a slice is sorted in ascending order. func AssertOrderedAscending[T cmp.Ordered](t *testing.T, values []T, fieldName string) { t.Helper() assert.True(t, slices.IsSorted(values), "%s should be in ascending order, got: %v", fieldName, values) } -// AssertOrderedDescending validates that a slice is sorted in descending order. func AssertOrderedDescending[T cmp.Ordered](t *testing.T, values []T, fieldName string) { t.Helper() reversed := slices.Clone(values) @@ -120,7 +100,6 @@ func AssertOrderedDescending[T cmp.Ordered](t *testing.T, values []T, fieldName assert.True(t, slices.IsSorted(reversed), "%s should be in descending order, got: %v", fieldName, values) } -// AssertTimesOrderedAscending validates that times are in ascending order. func AssertTimesOrderedAscending(t *testing.T, times []time.Time, fieldName string) { t.Helper() isSorted := slices.IsSortedFunc(times, func(a, b time.Time) int { @@ -129,7 +108,6 @@ func AssertTimesOrderedAscending(t *testing.T, times []time.Time, fieldName stri assert.True(t, isSorted, "%s should be in ascending order", fieldName) } -// AssertTimesOrderedDescending validates that times are in descending order. func AssertTimesOrderedDescending(t *testing.T, times []time.Time, fieldName string) { t.Helper() isSorted := slices.IsSortedFunc(times, func(a, b time.Time) int { @@ -138,8 +116,6 @@ func AssertTimesOrderedDescending(t *testing.T, times []time.Time, fieldName str assert.True(t, isSorted, "%s should be in descending order", fieldName) } -// AssertNodeNotAccessible validates that a node query returns nil or an error -// (used for tenant isolation tests). func AssertNodeNotAccessible(t *testing.T, err error, nodeIsNil bool, resourceType string) { t.Helper() if err == nil { @@ -148,13 +124,11 @@ func AssertNodeNotAccessible(t *testing.T, err error, nodeIsNil bool, resourceTy // If there's an error, that's also acceptable (access denied) } -// RequireForbiddenError asserts that the error is a GraphQL FORBIDDEN error. func RequireForbiddenError(t *testing.T, err error, msgAndArgs ...any) { t.Helper() RequireErrorCode(t, err, "FORBIDDEN", msgAndArgs...) } -// RequireErrorCode asserts that the error is a GraphQL error with the specified code. func RequireErrorCode(t *testing.T, err error, code string, msgAndArgs ...any) { t.Helper() require.Error(t, err, msgAndArgs...) diff --git a/e2e/internal/testutil/client.go b/e2e/internal/testutil/client.go index 9c4f9d392..204f1f4ee 100644 --- a/e2e/internal/testutil/client.go +++ b/e2e/internal/testutil/client.go @@ -31,16 +31,12 @@ import ( "go.probo.inc/probo/pkg/gid" ) -// generateUniqueID creates a unique identifier for test isolation. -// It combines a timestamp with random bytes to ensure uniqueness -// even when tests run in parallel. func generateUniqueID() string { randomBytes := make([]byte, 4) rand.Read(randomBytes) return fmt.Sprintf("%d-%s", time.Now().UnixNano(), hex.EncodeToString(randomBytes)) } -// TestRole represents the role a test user should have type TestRole string const ( @@ -49,7 +45,6 @@ const ( RoleViewer TestRole = "VIEWER" ) -// Client is an authenticated HTTP client for making API requests type Client struct { T testing.TB httpClient *http.Client @@ -59,8 +54,6 @@ type Client struct { organizationID gid.GID } -// NewClient creates a new authenticated test client with the specified role. -// It creates a new user, organization, and sets up the membership with the given role. func NewClient(t testing.TB, role TestRole) *Client { t.Helper() @@ -82,8 +75,6 @@ func NewClient(t testing.TB, role TestRole) *Client { return client } -// NewClientInOrg creates a test client for a user in an existing organization. -// The ownerClient must be an OWNER of the organization to invite the new user. func NewClientInOrg(t testing.TB, role TestRole, ownerClient *Client) *Client { t.Helper() @@ -335,17 +326,14 @@ func (c *Client) acceptInvitation(invitationID gid.GID) { require.NoError(c.T, err, "acceptInvitation mutation failed") } -// GetUserID returns the authenticated user's ID func (c *Client) GetUserID() gid.GID { return c.userID } -// GetOrganizationID returns the test organization's ID func (c *Client) GetOrganizationID() gid.GID { return c.organizationID } -// GetRole returns the client's role func (c *Client) GetRole() TestRole { return c.role } diff --git a/e2e/internal/testutil/graphql.go b/e2e/internal/testutil/graphql.go index c6724cc89..9652ed929 100644 --- a/e2e/internal/testutil/graphql.go +++ b/e2e/internal/testutil/graphql.go @@ -26,19 +26,16 @@ import ( "github.com/stretchr/testify/require" ) -// GraphQLRequest represents a GraphQL request payload. type GraphQLRequest struct { Query string `json:"query"` Variables map[string]any `json:"variables,omitempty"` } -// GraphQLResponse represents a GraphQL response. type GraphQLResponse struct { Data json.RawMessage `json:"data"` Errors []GraphQLError `json:"errors,omitempty"` } -// GraphQLError represents a GraphQL error. type GraphQLError struct { Message string `json:"message"` Path []any `json:"path,omitempty"` @@ -49,7 +46,6 @@ func (e GraphQLError) Error() string { return e.Message } -// Code returns the error code from extensions, or empty string if not present. func (e GraphQLError) Code() string { if e.Extensions == nil { return "" @@ -60,7 +56,6 @@ func (e GraphQLError) Code() string { return "" } -// GraphQLErrors is a collection of GraphQL errors. type GraphQLErrors []GraphQLError func (e GraphQLErrors) Error() string { @@ -73,7 +68,6 @@ func (e GraphQLErrors) Error() string { return fmt.Sprintf("%s (and %d more errors)", e[0].Message, len(e)-1) } -// Do executes a raw GraphQL request and returns the response. func (c *Client) Do(query string, variables map[string]any) (*GraphQLResponse, error) { reqBody := GraphQLRequest{ Query: query, @@ -118,30 +112,6 @@ func (c *Client) Do(query string, variables map[string]any) (*GraphQLResponse, e return &gqlResp, nil } -// Execute runs a GraphQL query/mutation and unmarshals the result into the provided struct. -// The result should be a pointer to a struct that matches the expected response shape. -// -// Example: -// -// var result struct { -// CreateVendor struct { -// VendorEdge struct { -// Node struct { -// ID string `json:"id"` -// Name string `json:"name"` -// } `json:"node"` -// } `json:"vendorEdge"` -// } `json:"createVendor"` -// } -// err := client.Execute(` -// mutation($input: CreateVendorInput!) { -// createVendor(input: $input) { -// vendorEdge { -// node { id name } -// } -// } -// } -// `, map[string]any{"input": map[string]any{"organizationId": orgID, "name": "AWS"}}, &result) func (c *Client) Execute(query string, variables map[string]any, result any) error { resp, err := c.Do(query, variables) if err != nil { @@ -157,15 +127,12 @@ func (c *Client) Execute(query string, variables map[string]any, result any) err return nil } -// MustExecute runs a GraphQL query/mutation and fails the test if there's an error. func (c *Client) MustExecute(query string, variables map[string]any, result any) { c.T.Helper() err := c.Execute(query, variables, result) require.NoError(c.T, err, "GraphQL request failed") } -// ExecuteShouldFail runs a GraphQL query/mutation and expects it to return an error. -// Returns the error for further assertions. func (c *Client) ExecuteShouldFail(query string, variables map[string]any) error { c.T.Helper() _, err := c.Do(query, variables) @@ -173,31 +140,24 @@ func (c *Client) ExecuteShouldFail(query string, variables map[string]any) error return err } -// HTTPClient returns the underlying HTTP client for making non-GraphQL requests. func (c *Client) HTTPClient() *http.Client { return c.httpClient } -// BaseURL returns the base URL of the test server. func (c *Client) BaseURL() string { return c.baseURL } -// UploadFile represents a file to be uploaded in a GraphQL mutation. type UploadFile struct { Filename string ContentType string Content []byte } -// ExecuteWithFile runs a GraphQL mutation with a file upload using multipart form data. -// The variablePath specifies where in the variables the file should be placed (e.g., "input.file"). func (c *Client) ExecuteWithFile(query string, variables map[string]any, variablePath string, file UploadFile, result any) error { return c.executeMultipart(query, variables, map[string]UploadFile{variablePath: file}, result) } -// ExecuteWithFiles runs a GraphQL mutation with multiple file uploads using multipart form data. -// The files map specifies variable paths to files (e.g., {"input.file": file1, "input.attachment": file2}). func (c *Client) ExecuteWithFiles(query string, variables map[string]any, files map[string]UploadFile, result any) error { return c.executeMultipart(query, variables, files, result) } diff --git a/e2e/internal/testutil/testutil.go b/e2e/internal/testutil/testutil.go index 4e0f03d33..fe39c44af 100644 --- a/e2e/internal/testutil/testutil.go +++ b/e2e/internal/testutil/testutil.go @@ -12,33 +12,6 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -// Package testutil provides end-to-end testing infrastructure for the Probo API. -// -// This package runs an external probod binary for realistic e2e testing. -// It supports coverage collection when using a coverage-instrumented binary. -// -// Required environment variables: -// - PROBO_E2E_BINARY: Path to the probod binary -// - PROBO_E2E_CONFIG: Path to the config file -// -// Optional environment variables: -// - PROBO_E2E_COVERDIR: Directory for coverage data (enables coverage collection) -// - PROBO_E2E_VERBOSE: If set, outputs binary stdout/stderr for debugging -// -// Example usage: -// -// # Build the binary (with coverage) -// go build -cover -o bin/probod-coverage ./cmd/probod -// -// # Run e2e tests -// PROBO_E2E_BINARY=./bin/probod-coverage \ -// PROBO_E2E_COVERDIR=./coverage/e2e \ -// PROBO_E2E_CONFIG=./e2e/console/testdata/config.yaml \ -// go test -v ./e2e/console/... -// -// # Generate coverage report -// go tool covdata textfmt -i=./coverage/e2e -o=coverage-e2e.out -// go tool cover -html=coverage-e2e.out -o=coverage-e2e.html package testutil import ( @@ -58,24 +31,12 @@ var ( setupOnce sync.Once ) -// TestEnv holds the test environment state type TestEnv struct { BaseURL string cmd *exec.Cmd done chan error } -// Setup initializes the test environment. Call this from TestMain. -// It starts probod with the provided configuration and waits for it to be ready. -// -// Example: -// -// func TestMain(m *testing.M) { -// testutil.Setup() -// code := m.Run() -// testutil.Teardown() -// os.Exit(code) -// } func Setup() { setupOnce.Do(func() { binaryPath := os.Getenv("PROBO_E2E_BINARY") @@ -104,11 +65,6 @@ func Setup() { done: make(chan error, 1), } - // Start the external binary - // Note: We use exec.Command instead of exec.CommandContext because - // CommandContext sends SIGKILL on context cancel, which prevents the - // binary from writing coverage data. We manage the process lifecycle - // manually in Teardown() using SIGTERM for graceful shutdown. cmd := exec.Command(binaryPath, "-cfg-file", configPath) if coverDir != "" { cmd.Env = append(os.Environ(), "GOCOVERDIR="+coverDir) @@ -130,16 +86,13 @@ func Setup() { os.Exit(1) } - // Wait for process to exit in background go func() { err := cmd.Wait() testEnv.done <- err }() - // TODO: Parse config file to get actual port testEnv.BaseURL = "http://localhost:18080" - // Wait for server to be ready ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := waitForServer(ctx, testEnv.BaseURL, 30*time.Second); err != nil { @@ -179,17 +132,14 @@ func waitForServer(ctx context.Context, baseURL string, timeout time.Duration) e return fmt.Errorf("server did not become ready within %v", timeout) } -// Teardown shuts down the test environment. Call this after m.Run() in TestMain. func Teardown() { if testEnv == nil { return } if testEnv.cmd != nil && testEnv.cmd.Process != nil { - // Send SIGTERM for graceful shutdown (allows coverage data to be written) testEnv.cmd.Process.Signal(syscall.SIGTERM) - // Wait for graceful shutdown with timeout select { case <-testEnv.done: case <-time.After(10 * time.Second): @@ -199,7 +149,6 @@ func Teardown() { } } -// GetBaseURL returns the base URL of the test server func GetBaseURL() string { if testEnv == nil { return "http://localhost:8080"