From 4225ae68ba6213d29c6d7be28bfca1b61a20841f Mon Sep 17 00:00:00 2001 From: Taichi Sasaki Date: Wed, 17 Sep 2025 01:16:37 +0900 Subject: [PATCH 1/5] Add test for generateHarness() in testing/codegen --- testing/codegen/harness_test.go | 57 +++++++ testing/codegen/testdata/code.go | 253 +++++++++++++++++++++++++++++++ testing/codegen/testdata/dsls.go | 26 ++++ 3 files changed, 336 insertions(+) create mode 100644 testing/codegen/harness_test.go create mode 100644 testing/codegen/testdata/code.go create mode 100644 testing/codegen/testdata/dsls.go diff --git a/testing/codegen/harness_test.go b/testing/codegen/harness_test.go new file mode 100644 index 000000000..e8921a252 --- /dev/null +++ b/testing/codegen/harness_test.go @@ -0,0 +1,57 @@ +package codegen + +import ( + "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" + "goa.design/plugins/v3/testing/codegen/testdata" +) + +func TestGenerateHarness(t *testing.T) { + cases := map[string]struct { + DSL func() + Code map[string][]string + Path string + }{ + "with-stream": { + DSL: testdata.WithStreamDSL, + Code: map[string][]string{ + "http-harness": {testdata.WithStreamCode}, + }, + Path: "gen/with_stream_service/with_stream_servicetest/harness.go", + }, + "without-stream": { + DSL: testdata.WithoutStreamDSL, + Code: map[string][]string{ + "http-harness": {testdata.WithoutStreamCode}, + }, + Path: "gen/without_stream_service/without_stream_servicetest/harness.go", + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + root := httpcodegen.RunHTTPDSL(t, c.DSL) + services := service.NewServicesData(root) + svc := root.Services[0] + svcData := services.Get(svc.Name) + f := generateHarness("", svcData, root, svc) + assert.Equal(t, c.Path, f.Path) + for sec, secCode := range c.Code { + testCode(t, f, sec, secCode) + } + }) + } +} + +func testCode(t *testing.T, file *codegen.File, section string, expCode []string) { + sections := file.Section(section) + require.Len(t, sections, len(expCode)) + for i, c := range expCode { + code := codegen.SectionCode(t, sections[i]) + assert.Equal(t, c, code) + } +} diff --git a/testing/codegen/testdata/code.go b/testing/codegen/testdata/code.go new file mode 100644 index 000000000..d02878328 --- /dev/null +++ b/testing/codegen/testdata/code.go @@ -0,0 +1,253 @@ +package testdata + +var WithStreamCode = `// setupHTTP initializes the HTTP test server and client. +func (h *Harness) setupHTTP() { + // Create endpoints + endpoints := withstreamservice.NewEndpoints(h.service) + + // Create HTTP handler + mux := goahttp.NewMuxer() + // Create WebSocket upgrader for streaming endpoints + upgrader := &websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + } + server := httpsvr.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil, upgrader, nil) + httpsvr.Mount(mux, server) + + // Create test server + h.httpSvr = httptest.NewServer(mux) + + // Create HTTP client + h.httpCli = &http.Client{ + Timeout: 10 * time.Second, + } +} + +// HTTPClient returns an HTTP client configured for the test server. +func (h *Harness) HTTPClient() *http.Client { + if h.httpCli == nil { + h.t.Fatal("HTTP transport not configured") + } + return h.httpCli +} + +// getHTTPClientImpl returns the underlying HTTP client implementation. +func (h *Harness) getHTTPClientImpl() *httpcli.Client { + if h.httpSvr == nil || h.httpCli == nil { + h.t.Fatal("HTTP transport not configured") + } + u, err := url.Parse(h.httpSvr.URL) + if err != nil { + h.t.Fatalf("invalid test server URL: %v", err) + } + scheme := u.Scheme + host := u.Host + // Create WebSocket dialer for streaming endpoints + wsDialer := &websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + } + + return httpcli.NewClient( + scheme, + host, + h.httpCli, + goahttp.RequestEncoder, + goahttp.ResponseDecoder, + false, + wsDialer, + nil, + ) +} + +// HTTPClientEndpoints creates HTTP client endpoints for the service. +func (h *Harness) HTTPClientEndpoints() *withstreamservice.Endpoints { + c := h.getHTTPClientImpl() + return &withstreamservice.Endpoints{ + WithStreamMethod: c.WithStreamMethod(), + } +} + +// HTTPURL returns the base URL of the test HTTP server. +func (h *Harness) HTTPURL() string { + if h.httpSvr == nil { + h.t.Fatal("HTTP transport not configured") + } + return h.httpSvr.URL +} + +// HTTPRequest creates a new HTTP request for testing. +func (h *Harness) HTTPRequest(method, path string, body any) *http.Request { + h.t.Helper() + + url := h.HTTPURL() + path + + var bodyReader io.Reader + if body != nil { + data, err := json.Marshal(body) + if err != nil { + h.t.Fatalf("Failed to marshal request body: %v", err) + } + bodyReader = bytes.NewReader(data) + } + + req, err := http.NewRequestWithContext(h.ctx, method, url, bodyReader) + if err != nil { + h.t.Fatalf("Failed to create request: %v", err) + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req +} + +// HTTPWSURL builds a websocket URL from the base HTTP URL and path. +func (h *Harness) HTTPWSURL(path string) string { + base := h.HTTPURL() + u, err := url.Parse(base) + if err != nil { + h.t.Fatalf("invalid base URL: %v", err) + } + if u.Scheme == "https" { + u.Scheme = "wss" + } else { + u.Scheme = "ws" + } + u.Path = path + return u.String() +} + +// HTTPDo performs an HTTP request and returns the response. +func (h *Harness) HTTPDo(req *http.Request) *http.Response { + h.t.Helper() + + resp, err := h.HTTPClient().Do(req) + if err != nil { + h.t.Fatalf("HTTP request failed: %v", err) + } + + return resp +} +` + +var WithoutStreamCode = `// setupHTTP initializes the HTTP test server and client. +func (h *Harness) setupHTTP() { + // Create endpoints + endpoints := withoutstreamservice.NewEndpoints(h.service) + + // Create HTTP handler + mux := goahttp.NewMuxer() + server := httpsvr.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil) + httpsvr.Mount(mux, server) + + // Create test server + h.httpSvr = httptest.NewServer(mux) + + // Create HTTP client + h.httpCli = &http.Client{ + Timeout: 10 * time.Second, + } +} + +// HTTPClient returns an HTTP client configured for the test server. +func (h *Harness) HTTPClient() *http.Client { + if h.httpCli == nil { + h.t.Fatal("HTTP transport not configured") + } + return h.httpCli +} + +// getHTTPClientImpl returns the underlying HTTP client implementation. +func (h *Harness) getHTTPClientImpl() *httpcli.Client { + if h.httpSvr == nil || h.httpCli == nil { + h.t.Fatal("HTTP transport not configured") + } + u, err := url.Parse(h.httpSvr.URL) + if err != nil { + h.t.Fatalf("invalid test server URL: %v", err) + } + scheme := u.Scheme + host := u.Host + + return httpcli.NewClient( + scheme, + host, + h.httpCli, + goahttp.RequestEncoder, + goahttp.ResponseDecoder, + false, + ) +} + +// HTTPClientEndpoints creates HTTP client endpoints for the service. +func (h *Harness) HTTPClientEndpoints() *withoutstreamservice.Endpoints { + c := h.getHTTPClientImpl() + return &withoutstreamservice.Endpoints{ + WithoutStreamMethod: c.WithoutStreamMethod(), + } +} + +// HTTPURL returns the base URL of the test HTTP server. +func (h *Harness) HTTPURL() string { + if h.httpSvr == nil { + h.t.Fatal("HTTP transport not configured") + } + return h.httpSvr.URL +} + +// HTTPRequest creates a new HTTP request for testing. +func (h *Harness) HTTPRequest(method, path string, body any) *http.Request { + h.t.Helper() + + url := h.HTTPURL() + path + + var bodyReader io.Reader + if body != nil { + data, err := json.Marshal(body) + if err != nil { + h.t.Fatalf("Failed to marshal request body: %v", err) + } + bodyReader = bytes.NewReader(data) + } + + req, err := http.NewRequestWithContext(h.ctx, method, url, bodyReader) + if err != nil { + h.t.Fatalf("Failed to create request: %v", err) + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req +} + +// HTTPWSURL builds a websocket URL from the base HTTP URL and path. +func (h *Harness) HTTPWSURL(path string) string { + base := h.HTTPURL() + u, err := url.Parse(base) + if err != nil { + h.t.Fatalf("invalid base URL: %v", err) + } + if u.Scheme == "https" { + u.Scheme = "wss" + } else { + u.Scheme = "ws" + } + u.Path = path + return u.String() +} + +// HTTPDo performs an HTTP request and returns the response. +func (h *Harness) HTTPDo(req *http.Request) *http.Response { + h.t.Helper() + + resp, err := h.HTTPClient().Do(req) + if err != nil { + h.t.Fatalf("HTTP request failed: %v", err) + } + + return resp +} +` diff --git a/testing/codegen/testdata/dsls.go b/testing/codegen/testdata/dsls.go new file mode 100644 index 000000000..fb41be800 --- /dev/null +++ b/testing/codegen/testdata/dsls.go @@ -0,0 +1,26 @@ +package testdata + +import ( + . "goa.design/goa/v3/dsl" +) + +var WithStreamDSL = func() { + Service("WithStreamService", func() { + Method("WithStreamMethod", func() { + StreamingPayload(String) + HTTP(func() { + GET("/") + }) + }) + }) +} + +var WithoutStreamDSL = func() { + Service("WithoutStreamService", func() { + Method("WithoutStreamMethod", func() { + HTTP(func() { + GET("/") + }) + }) + }) +} From d9c2bdbaef1321c3cb996e892e165d4bffb97a4c Mon Sep 17 00:00:00 2001 From: Taichi Sasaki Date: Wed, 17 Sep 2025 01:34:22 +0900 Subject: [PATCH 2/5] Support methods without streams by testing/codegen --- testing/codegen/templates/http_harness.go.tpl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/testing/codegen/templates/http_harness.go.tpl b/testing/codegen/templates/http_harness.go.tpl index 1f5b62644..a665df05c 100644 --- a/testing/codegen/templates/http_harness.go.tpl +++ b/testing/codegen/templates/http_harness.go.tpl @@ -5,11 +5,13 @@ func (h *Harness) setupHTTP() { // Create HTTP handler mux := goahttp.NewMuxer() + {{- if .HasStreams }} // Create WebSocket upgrader for streaming endpoints upgrader := &websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } - server := httpsvr.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil, upgrader, nil) + {{- end }} + server := httpsvr.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil{{ if .HasStreams }}, upgrader, nil{{ end }}) httpsvr.Mount(mux, server) // Create test server @@ -40,10 +42,12 @@ func (h *Harness) getHTTPClientImpl() *httpcli.Client { } scheme := u.Scheme host := u.Host + {{- if .HasStreams }} // Create WebSocket dialer for streaming endpoints wsDialer := &websocket.Dialer{ Proxy: http.ProxyFromEnvironment, } + {{- end }} return httpcli.NewClient( scheme, @@ -52,8 +56,10 @@ func (h *Harness) getHTTPClientImpl() *httpcli.Client { goahttp.RequestEncoder, goahttp.ResponseDecoder, false, + {{- if .HasStreams }} wsDialer, nil, + {{- end }} ) } From 6e439db1965584ea74ff93be9580b8ecf4d812b2 Mon Sep 17 00:00:00 2001 From: Taichi Sasaki Date: Wed, 17 Sep 2025 01:34:48 +0900 Subject: [PATCH 3/5] Fix to tabbed indentation --- testing/codegen/templates/http_harness.go.tpl | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/testing/codegen/templates/http_harness.go.tpl b/testing/codegen/templates/http_harness.go.tpl index a665df05c..21e4d05b8 100644 --- a/testing/codegen/templates/http_harness.go.tpl +++ b/testing/codegen/templates/http_harness.go.tpl @@ -33,50 +33,50 @@ func (h *Harness) HTTPClient() *http.Client { {{ printf "getHTTPClientImpl returns the underlying HTTP client implementation." | comment }} func (h *Harness) getHTTPClientImpl() *httpcli.Client { - if h.httpSvr == nil || h.httpCli == nil { - h.t.Fatal("HTTP transport not configured") - } - u, err := url.Parse(h.httpSvr.URL) - if err != nil { - h.t.Fatalf("invalid test server URL: %v", err) - } - scheme := u.Scheme - host := u.Host - {{- if .HasStreams }} - // Create WebSocket dialer for streaming endpoints - wsDialer := &websocket.Dialer{ - Proxy: http.ProxyFromEnvironment, - } - {{- end }} - - return httpcli.NewClient( - scheme, - host, - h.httpCli, - goahttp.RequestEncoder, - goahttp.ResponseDecoder, - false, - {{- if .HasStreams }} - wsDialer, - nil, - {{- end }} - ) + if h.httpSvr == nil || h.httpCli == nil { + h.t.Fatal("HTTP transport not configured") + } + u, err := url.Parse(h.httpSvr.URL) + if err != nil { + h.t.Fatalf("invalid test server URL: %v", err) + } + scheme := u.Scheme + host := u.Host + {{- if .HasStreams }} + // Create WebSocket dialer for streaming endpoints + wsDialer := &websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + } + {{- end }} + + return httpcli.NewClient( + scheme, + host, + h.httpCli, + goahttp.RequestEncoder, + goahttp.ResponseDecoder, + false, + {{- if .HasStreams }} + wsDialer, + nil, + {{- end }} + ) } {{ printf "HTTPClientEndpoints creates HTTP client endpoints for the service." | comment }} func (h *Harness) HTTPClientEndpoints() *{{ .PkgName }}.Endpoints { - c := h.getHTTPClientImpl() - return &{{ .PkgName }}.Endpoints{ - {{- range .Methods }} - {{- $method := . }} - {{- range .Targets }} - {{- if or .IsHTTPPlain .IsHTTPServerSent .IsHTTPWebSocket }} - {{ $method.VarName }}: c.{{ $method.VarName }}(), - {{- break }} - {{- end }} - {{- end }} - {{- end }} - } + c := h.getHTTPClientImpl() + return &{{ .PkgName }}.Endpoints{ + {{- range .Methods }} + {{- $method := . }} + {{- range .Targets }} + {{- if or .IsHTTPPlain .IsHTTPServerSent .IsHTTPWebSocket }} + {{ $method.VarName }}: c.{{ $method.VarName }}(), + {{- break }} + {{- end }} + {{- end }} + {{- end }} + } } {{ printf "HTTPURL returns the base URL of the test HTTP server." | comment }} From 7108db7953dca2684021392d7bca28c2d7a429b0 Mon Sep 17 00:00:00 2001 From: Taichi Sasaki Date: Wed, 17 Sep 2025 01:34:51 +0900 Subject: [PATCH 4/5] Use IsStreaming() instead of NoStreamKind --- testing/codegen/harness.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/codegen/harness.go b/testing/codegen/harness.go index ae018dc27..018be4c58 100644 --- a/testing/codegen/harness.go +++ b/testing/codegen/harness.go @@ -206,7 +206,7 @@ func testingPath(_ string, svc *expr.ServiceExpr) string { // hasStreams checks if the service has streaming methods. func hasStreams(svc *expr.ServiceExpr) bool { for _, m := range svc.Methods { - if m.Stream != expr.NoStreamKind { + if m.IsStreaming() { return true } } From 5b4d2784efc47efe25a0fb95fead1b6fb51831d1 Mon Sep 17 00:00:00 2001 From: Taichi Sasaki Date: Wed, 17 Sep 2025 01:50:21 +0900 Subject: [PATCH 5/5] Generate HTTPWSURL() only for methods with streams --- testing/codegen/templates/http_harness.go.tpl | 2 ++ testing/codegen/testdata/code.go | 16 ---------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/testing/codegen/templates/http_harness.go.tpl b/testing/codegen/templates/http_harness.go.tpl index 21e4d05b8..a13e8e172 100644 --- a/testing/codegen/templates/http_harness.go.tpl +++ b/testing/codegen/templates/http_harness.go.tpl @@ -114,6 +114,7 @@ func (h *Harness) HTTPRequest(method, path string, body any) *http.Request { return req } +{{- if .HasStreams }} {{ printf "HTTPWSURL builds a websocket URL from the base HTTP URL and path." | comment }} func (h *Harness) HTTPWSURL(path string) string { base := h.HTTPURL() @@ -129,6 +130,7 @@ func (h *Harness) HTTPWSURL(path string) string { u.Path = path return u.String() } +{{- end }} {{ printf "HTTPDo performs an HTTP request and returns the response." | comment }} func (h *Harness) HTTPDo(req *http.Request) *http.Response { diff --git a/testing/codegen/testdata/code.go b/testing/codegen/testdata/code.go index d02878328..bc9707d1c 100644 --- a/testing/codegen/testdata/code.go +++ b/testing/codegen/testdata/code.go @@ -223,22 +223,6 @@ func (h *Harness) HTTPRequest(method, path string, body any) *http.Request { return req } -// HTTPWSURL builds a websocket URL from the base HTTP URL and path. -func (h *Harness) HTTPWSURL(path string) string { - base := h.HTTPURL() - u, err := url.Parse(base) - if err != nil { - h.t.Fatalf("invalid base URL: %v", err) - } - if u.Scheme == "https" { - u.Scheme = "wss" - } else { - u.Scheme = "ws" - } - u.Path = path - return u.String() -} - // HTTPDo performs an HTTP request and returns the response. func (h *Harness) HTTPDo(req *http.Request) *http.Response { h.t.Helper()