diff --git a/testing/codegen/scenarios.go b/testing/codegen/scenarios.go index ac3b1175..6bbf65ae 100644 --- a/testing/codegen/scenarios.go +++ b/testing/codegen/scenarios.go @@ -2,8 +2,10 @@ package codegen import ( "fmt" + "maps" "os" "path/filepath" + "slices" "goa.design/goa/v3/codegen" "goa.design/goa/v3/codegen/service" @@ -173,9 +175,7 @@ func buildScenariosData(svcData *service.Data, root *expr.RootExpr, svc *expr.Se transportSet["jsonrpc-sse"] = true transportSet["jsonrpc-ws"] = true } - for t := range transportSet { - data.ValidTransports = append(data.ValidTransports, t) - } + data.ValidTransports = slices.Sorted(maps.Keys(transportSet)) // Build method data with available transports for i, m := range svc.Methods { @@ -217,9 +217,7 @@ func buildScenariosData(svcData *service.Data, root *expr.RootExpr, svc *expr.Se } // Convert set to sorted list - for transport := range transportSet { - md.Transports = append(md.Transports, transport) - } + md.Transports = slices.Sorted(maps.Keys(transportSet)) data.Methods = append(data.Methods, md) } diff --git a/testing/codegen/scenarios_test.go b/testing/codegen/scenarios_test.go index bf01a8de..e99028cf 100644 --- a/testing/codegen/scenarios_test.go +++ b/testing/codegen/scenarios_test.go @@ -1,9 +1,11 @@ package codegen import ( + "bytes" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "goa.design/goa/v3/codegen" "goa.design/goa/v3/codegen/service" httpcodegen "goa.design/goa/v3/http/codegen" @@ -19,6 +21,7 @@ func TestGenerateScenarios(t *testing.T) { "with-payload": { DSL: testdata.WithPayloadDSL, Code: map[string][]string{ + "scenario-types": {testdata.ScenarioTypesWithPayloadCode}, "scenario-runner": {testdata.ScenarioRunnerWithPayloadCode}, }, Path: "gen/with_payload_service/with_payload_servicetest/scenarios.go", @@ -26,6 +29,7 @@ func TestGenerateScenarios(t *testing.T) { "with-result": { DSL: testdata.WithResultDSL, Code: map[string][]string{ + "scenario-types": {testdata.ScenarioTypesWithResultCode}, "scenario-runner": {testdata.ScenarioRunnerWithResultCode}, }, Path: "gen/with_result_service/with_result_servicetest/scenarios.go", @@ -33,6 +37,7 @@ func TestGenerateScenarios(t *testing.T) { "without-payload-result": { DSL: testdata.WithoutPayloadResultDSL, Code: map[string][]string{ + "scenario-types": {testdata.ScenarioTypesWithoutPayloadResultCode}, "scenario-runner": {testdata.ScenarioRunnerWithoutPayloadResultCode}, }, Path: "gen/without_payload_result_service/without_payload_result_servicetest/scenarios.go", @@ -77,3 +82,46 @@ func TestGenerateScenarios_ArrayResultTypeAssertion(t *testing.T) { assert.NotContains(t, code, svcData.PkgName+".[]") assert.NotContains(t, code, "*"+svcData.PkgName+".[]") } + +func TestGenerateExampleScenarios(t *testing.T) { + cases := map[string]struct { + DSL func() + Code map[string][]string + }{ + "with-payload": { + DSL: testdata.WithPayloadDSL, + Code: map[string][]string{ + "example-scenarios": {testdata.ExampleScenariosWithPayloadCode}, + }, + }, + "with-result": { + DSL: testdata.WithResultDSL, + Code: map[string][]string{ + "example-scenarios": {testdata.ExampleScenariosWithResultCode}, + }, + }, + "without-payload-result": { + DSL: testdata.WithoutPayloadResultDSL, + Code: map[string][]string{ + "example-scenarios": {testdata.ExampleScenariosWithoutPayloadResultCode}, + }, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + root := httpcodegen.RunHTTPDSL(t, c.DSL) + svc := root.Services[0] + f := generateExampleScenarios("", root, svc) + assert.Equal(t, "scenarios.yaml", f.Path) + for sec, secCode := range c.Code { + sections := f.Section(sec) + require.Len(t, sections, len(secCode)) + for i, c := range secCode { + var buf bytes.Buffer + assert.NoError(t, sections[i].Write(&buf)) + assert.Equal(t, c, buf.String()) + } + } + }) + } +} diff --git a/testing/codegen/testdata/code.go b/testing/codegen/testdata/code.go index 70577be3..4e526fa6 100644 --- a/testing/codegen/testdata/code.go +++ b/testing/codegen/testdata/code.go @@ -379,6 +379,67 @@ func (h *Harness) HTTPDo(req *http.Request) *http.Response { } ` +var ScenarioTypesWithPayloadCode = `// Scenario defines a test scenario that can be loaded from YAML. +type Scenario struct { + Name string ` + "`yaml:\"name\"`" + ` + Description string ` + "`yaml:\"description,omitempty\"`" + ` + Transport string ` + "`yaml:\"transport,omitempty\"`" + ` // Default transport for all steps + Timeout string ` + "`yaml:\"timeout,omitempty\"`" + ` // Default timeout for all steps + Steps []Step ` + "`yaml:\"steps\"`" + ` +} + +// Step defines a single step in a test scenario. +type Step struct { + Method string ` + "`yaml:\"method\"`" + ` + Transport string ` + "`yaml:\"transport,omitempty\"`" + ` // Override transport for this step + Payload map[string]any ` + "`yaml:\"payload,omitempty\"`" + ` + Stream bool ` + "`yaml:\"stream,omitempty\"`" + ` // For mixed results, use streaming variant + Send []map[string]any ` + "`yaml:\"send,omitempty\"`" + ` // For client/bidi streaming + Receive []map[string]any ` + "`yaml:\"receive,omitempty\"`" + ` // For server/bidi streaming + Expect Expectation ` + "`yaml:\"expect,omitempty\"`" + ` + Timeout string ` + "`yaml:\"timeout,omitempty\"`" + ` // Timeout duration (e.g., '10s', '1m') +} + +// Expectation defines expected outcomes for a step. +type Expectation struct { + Result map[string]any ` + "`yaml:\"result,omitempty\"`" + ` + Error string ` + "`yaml:\"error,omitempty\"`" + ` + Stream []map[string]any ` + "`yaml:\"stream,omitempty\"`" + ` // Expected stream messages + Validator string ` + "`yaml:\"validator,omitempty\"`" + ` // Custom validator function name + ValidatorPkg string ` + "`yaml:\"validator_pkg,omitempty\"`" + ` // Package containing validator (defaults to service package) +} + +// ScenarioConfig holds scenarios loaded from YAML. +type ScenarioConfig struct { + Scenarios []Scenario ` + "`yaml:\"scenarios\"`" + ` + Validators Validators ` + "`yaml:\"validators,omitempty\"`" + ` // Global validator configuration +} + +// Validators defines custom validation functions to use. +type Validators struct { + Package string ` + "`yaml:\"package,omitempty\"`" + ` // Default package for validators + Path string ` + "`yaml:\"path,omitempty\"`" + ` // Import path for validator package +} + +// Valid transport values for scenarios based on service configuration. +// Methods may support different combinations of these transports. +var ValidTransports = []string{ + "auto", // Use default/first available + "http", // HTTP plain (non-streaming methods only) + "http-sse", // HTTP Server-Sent Events (server streaming) + "http-ws", // HTTP WebSocket (client/server/bidi streaming) + "grpc", // gRPC (all streaming modes) + "jsonrpc", // JSON-RPC over HTTP (non-streaming) + "jsonrpc-sse", // JSON-RPC over SSE (server streaming) + "jsonrpc-ws", // JSON-RPC over WebSocket (streaming only) +} + +// TransportAvailability documents which transports each method supports. +var TransportAvailability = map[string][]string{ + "WithPayloadMethod": {"grpc", "http", "jsonrpc"}, +} +` + var ScenarioRunnerWithPayloadCode = `// ScenarioRunner executes test scenarios. type ScenarioRunner struct { scenarios []Scenario @@ -672,6 +733,67 @@ func (r *ScenarioRunner) selectTransport(client *Client, transport string) *Clie } ` +var ScenarioTypesWithResultCode = `// Scenario defines a test scenario that can be loaded from YAML. +type Scenario struct { + Name string ` + "`yaml:\"name\"`" + ` + Description string ` + "`yaml:\"description,omitempty\"`" + ` + Transport string ` + "`yaml:\"transport,omitempty\"`" + ` // Default transport for all steps + Timeout string ` + "`yaml:\"timeout,omitempty\"`" + ` // Default timeout for all steps + Steps []Step ` + "`yaml:\"steps\"`" + ` +} + +// Step defines a single step in a test scenario. +type Step struct { + Method string ` + "`yaml:\"method\"`" + ` + Transport string ` + "`yaml:\"transport,omitempty\"`" + ` // Override transport for this step + Payload map[string]any ` + "`yaml:\"payload,omitempty\"`" + ` + Stream bool ` + "`yaml:\"stream,omitempty\"`" + ` // For mixed results, use streaming variant + Send []map[string]any ` + "`yaml:\"send,omitempty\"`" + ` // For client/bidi streaming + Receive []map[string]any ` + "`yaml:\"receive,omitempty\"`" + ` // For server/bidi streaming + Expect Expectation ` + "`yaml:\"expect,omitempty\"`" + ` + Timeout string ` + "`yaml:\"timeout,omitempty\"`" + ` // Timeout duration (e.g., '10s', '1m') +} + +// Expectation defines expected outcomes for a step. +type Expectation struct { + Result map[string]any ` + "`yaml:\"result,omitempty\"`" + ` + Error string ` + "`yaml:\"error,omitempty\"`" + ` + Stream []map[string]any ` + "`yaml:\"stream,omitempty\"`" + ` // Expected stream messages + Validator string ` + "`yaml:\"validator,omitempty\"`" + ` // Custom validator function name + ValidatorPkg string ` + "`yaml:\"validator_pkg,omitempty\"`" + ` // Package containing validator (defaults to service package) +} + +// ScenarioConfig holds scenarios loaded from YAML. +type ScenarioConfig struct { + Scenarios []Scenario ` + "`yaml:\"scenarios\"`" + ` + Validators Validators ` + "`yaml:\"validators,omitempty\"`" + ` // Global validator configuration +} + +// Validators defines custom validation functions to use. +type Validators struct { + Package string ` + "`yaml:\"package,omitempty\"`" + ` // Default package for validators + Path string ` + "`yaml:\"path,omitempty\"`" + ` // Import path for validator package +} + +// Valid transport values for scenarios based on service configuration. +// Methods may support different combinations of these transports. +var ValidTransports = []string{ + "auto", // Use default/first available + "http", // HTTP plain (non-streaming methods only) + "http-sse", // HTTP Server-Sent Events (server streaming) + "http-ws", // HTTP WebSocket (client/server/bidi streaming) + "grpc", // gRPC (all streaming modes) + "jsonrpc", // JSON-RPC over HTTP (non-streaming) + "jsonrpc-sse", // JSON-RPC over SSE (server streaming) + "jsonrpc-ws", // JSON-RPC over WebSocket (streaming only) +} + +// TransportAvailability documents which transports each method supports. +var TransportAvailability = map[string][]string{ + "WithResultMethod": {"grpc", "http", "jsonrpc"}, +} +` + var ScenarioRunnerWithResultCode = `// ScenarioRunner executes test scenarios. type ScenarioRunner struct { scenarios []Scenario @@ -962,6 +1084,67 @@ func (r *ScenarioRunner) selectTransport(client *Client, transport string) *Clie } ` +var ScenarioTypesWithoutPayloadResultCode = `// Scenario defines a test scenario that can be loaded from YAML. +type Scenario struct { + Name string ` + "`yaml:\"name\"`" + ` + Description string ` + "`yaml:\"description,omitempty\"`" + ` + Transport string ` + "`yaml:\"transport,omitempty\"`" + ` // Default transport for all steps + Timeout string ` + "`yaml:\"timeout,omitempty\"`" + ` // Default timeout for all steps + Steps []Step ` + "`yaml:\"steps\"`" + ` +} + +// Step defines a single step in a test scenario. +type Step struct { + Method string ` + "`yaml:\"method\"`" + ` + Transport string ` + "`yaml:\"transport,omitempty\"`" + ` // Override transport for this step + Payload map[string]any ` + "`yaml:\"payload,omitempty\"`" + ` + Stream bool ` + "`yaml:\"stream,omitempty\"`" + ` // For mixed results, use streaming variant + Send []map[string]any ` + "`yaml:\"send,omitempty\"`" + ` // For client/bidi streaming + Receive []map[string]any ` + "`yaml:\"receive,omitempty\"`" + ` // For server/bidi streaming + Expect Expectation ` + "`yaml:\"expect,omitempty\"`" + ` + Timeout string ` + "`yaml:\"timeout,omitempty\"`" + ` // Timeout duration (e.g., '10s', '1m') +} + +// Expectation defines expected outcomes for a step. +type Expectation struct { + Result map[string]any ` + "`yaml:\"result,omitempty\"`" + ` + Error string ` + "`yaml:\"error,omitempty\"`" + ` + Stream []map[string]any ` + "`yaml:\"stream,omitempty\"`" + ` // Expected stream messages + Validator string ` + "`yaml:\"validator,omitempty\"`" + ` // Custom validator function name + ValidatorPkg string ` + "`yaml:\"validator_pkg,omitempty\"`" + ` // Package containing validator (defaults to service package) +} + +// ScenarioConfig holds scenarios loaded from YAML. +type ScenarioConfig struct { + Scenarios []Scenario ` + "`yaml:\"scenarios\"`" + ` + Validators Validators ` + "`yaml:\"validators,omitempty\"`" + ` // Global validator configuration +} + +// Validators defines custom validation functions to use. +type Validators struct { + Package string ` + "`yaml:\"package,omitempty\"`" + ` // Default package for validators + Path string ` + "`yaml:\"path,omitempty\"`" + ` // Import path for validator package +} + +// Valid transport values for scenarios based on service configuration. +// Methods may support different combinations of these transports. +var ValidTransports = []string{ + "auto", // Use default/first available + "http", // HTTP plain (non-streaming methods only) + "http-sse", // HTTP Server-Sent Events (server streaming) + "http-ws", // HTTP WebSocket (client/server/bidi streaming) + "grpc", // gRPC (all streaming modes) + "jsonrpc", // JSON-RPC over HTTP (non-streaming) + "jsonrpc-sse", // JSON-RPC over SSE (server streaming) + "jsonrpc-ws", // JSON-RPC over WebSocket (streaming only) +} + +// TransportAvailability documents which transports each method supports. +var TransportAvailability = map[string][]string{ + "WithoutPayloadResultMethod": {"grpc", "http", "jsonrpc"}, +} +` + var ScenarioRunnerWithoutPayloadResultCode = `// ScenarioRunner executes test scenarios. type ScenarioRunner struct { scenarios []Scenario @@ -1250,6 +1433,208 @@ func (r *ScenarioRunner) selectTransport(client *Client, transport string) *Clie } ` +var ExampleScenariosWithPayloadCode = `# Example test scenarios for WithPayloadService service +# This file demonstrates the YAML scenario testing capability. +# Customize these scenarios to match your testing needs. + +# Optional: Configure global validator settings +# validators: +# package: myvalidators # Package containing validator functions +# path: myapp/testing/validators # Import path for the package + +scenarios: + # Basic CRUD lifecycle test + - name: "basic_lifecycle" + description: "Tests basic service functionality" + transport: http # Optional: specify default transport (http, grpc, jsonrpc) + steps: + # Example step for WithPayloadMethod method + - method: WithPayloadMethod + payload: + # Add your test payload here + # Example: name: "test" + + # Transport-specific testing + - name: "http_specific" + description: "Tests HTTP-specific behavior" + transport: http + steps: + - method: WithPayloadMethod + payload: {} + - name: "grpc_specific" + description: "Tests gRPC-specific behavior" + transport: grpc + steps: + - method: WithPayloadMethod + payload: {} + + # Error handling test + - name: "error_handling" + description: "Tests error conditions" + steps: + +# Note: The scenario runner performs basic smoke testing. +# For detailed assertions and complex test logic, write custom Go tests. + +# Transport values (based on your service configuration): +# +# - auto +# +# - grpc +# +# - http +# +# - http-sse +# +# - http-ws +# +# - jsonrpc +# +# - jsonrpc-sse +# +# - jsonrpc-ws +# + +# Available methods and their transports: +# +# - WithPayloadMethod: grpc, http, jsonrpc +#` + +var ExampleScenariosWithResultCode = `# Example test scenarios for WithResultService service +# This file demonstrates the YAML scenario testing capability. +# Customize these scenarios to match your testing needs. + +# Optional: Configure global validator settings +# validators: +# package: myvalidators # Package containing validator functions +# path: myapp/testing/validators # Import path for the package + +scenarios: + # Basic CRUD lifecycle test + - name: "basic_lifecycle" + description: "Tests basic service functionality" + transport: http # Optional: specify default transport (http, grpc, jsonrpc) + steps: + # Example step for WithResultMethod method + - method: WithResultMethod + expect: + result: + # Add expected result fields here + # Example: status: "success" + # Optional: specify custom validator function + # validator: ValidateCustomResult # Function name in current package + # validator_pkg: myvalidators # Or specify different package + + # Transport-specific testing + - name: "http_specific" + description: "Tests HTTP-specific behavior" + transport: http + steps: + - method: WithResultMethod + expect: + result: {} + - name: "grpc_specific" + description: "Tests gRPC-specific behavior" + transport: grpc + steps: + - method: WithResultMethod + expect: + result: {} + + # Error handling test + - name: "error_handling" + description: "Tests error conditions" + steps: + +# Note: The scenario runner performs basic smoke testing. +# For detailed assertions and complex test logic, write custom Go tests. + +# Transport values (based on your service configuration): +# +# - auto +# +# - grpc +# +# - http +# +# - http-sse +# +# - http-ws +# +# - jsonrpc +# +# - jsonrpc-sse +# +# - jsonrpc-ws +# + +# Available methods and their transports: +# +# - WithResultMethod: grpc, http, jsonrpc +#` + +var ExampleScenariosWithoutPayloadResultCode = `# Example test scenarios for WithoutPayloadResultService service +# This file demonstrates the YAML scenario testing capability. +# Customize these scenarios to match your testing needs. + +# Optional: Configure global validator settings +# validators: +# package: myvalidators # Package containing validator functions +# path: myapp/testing/validators # Import path for the package + +scenarios: + # Basic CRUD lifecycle test + - name: "basic_lifecycle" + description: "Tests basic service functionality" + transport: http # Optional: specify default transport (http, grpc, jsonrpc) + steps: + # Example step for WithoutPayloadResultMethod method + - method: WithoutPayloadResultMethod + + # Transport-specific testing + - name: "http_specific" + description: "Tests HTTP-specific behavior" + transport: http + steps: + - method: WithoutPayloadResultMethod + - name: "grpc_specific" + description: "Tests gRPC-specific behavior" + transport: grpc + steps: + - method: WithoutPayloadResultMethod + + # Error handling test + - name: "error_handling" + description: "Tests error conditions" + steps: + +# Note: The scenario runner performs basic smoke testing. +# For detailed assertions and complex test logic, write custom Go tests. + +# Transport values (based on your service configuration): +# +# - auto +# +# - grpc +# +# - http +# +# - http-sse +# +# - http-ws +# +# - jsonrpc +# +# - jsonrpc-sse +# +# - jsonrpc-ws +# + +# Available methods and their transports: +# +# - WithoutPayloadResultMethod: grpc, http, jsonrpc +#` + var SuiteTestWithPayloadCode = `// RunWithPayloadServiceHarness exercises the generated harness against your // service implementation. // Call this helper from your test, passing your service implementation.