Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-01 12:17:54 +01:00
parent db3c3882d1
commit 4410fd0950
6 changed files with 0 additions and 183 deletions

View File

@@ -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 (

View File

@@ -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{}}
}

View File

@@ -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...)

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"