Skip to content

Commit d13465d

Browse files
Merge pull request #65 from kaleido-io/add_mtls_ffresty
feat: Add mTLS configuration for ffresty
2 parents f74fa67 + 089551f commit d13465d

File tree

13 files changed

+363
-93
lines changed

13 files changed

+363
-93
lines changed

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ require (
2828
github.com/x-cray/logrus-prefixed-formatter v0.5.2
2929
gitlab.com/hfuss/mux-prometheus v0.0.4
3030
golang.org/x/crypto v0.1.0
31-
golang.org/x/text v0.7.0
31+
golang.org/x/text v0.8.0
3232
gopkg.in/natefinch/lumberjack.v2 v2.0.0
3333
)
3434

@@ -70,9 +70,9 @@ require (
7070
github.com/stretchr/objx v0.5.0 // indirect
7171
github.com/subosito/gotenv v1.4.1 // indirect
7272
go.uber.org/atomic v1.10.0 // indirect
73-
golang.org/x/net v0.7.0 // indirect
74-
golang.org/x/sys v0.5.0 // indirect
75-
golang.org/x/term v0.5.0 // indirect
73+
golang.org/x/net v0.8.0 // indirect
74+
golang.org/x/sys v0.6.0 // indirect
75+
golang.org/x/term v0.6.0 // indirect
7676
google.golang.org/protobuf v1.28.1 // indirect
7777
gopkg.in/ini.v1 v1.67.0 // indirect
7878
gopkg.in/yaml.v2 v2.4.0 // indirect

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,8 +1358,8 @@ golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qx
13581358
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
13591359
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
13601360
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
1361-
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
1362-
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
1361+
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
1362+
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
13631363
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
13641364
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
13651365
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1514,15 +1514,15 @@ golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBc
15141514
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15151515
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15161516
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1517-
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
1518-
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1517+
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
1518+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15191519
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
15201520
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
15211521
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
15221522
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
15231523
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
1524-
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
1525-
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
1524+
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
1525+
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
15261526
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
15271527
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
15281528
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1532,8 +1532,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
15321532
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
15331533
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
15341534
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
1535-
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
1536-
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
1535+
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
1536+
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
15371537
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
15381538
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
15391539
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

pkg/ffapi/apiserver.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/getkin/kin-openapi/openapi3"
3232
"github.com/gorilla/mux"
3333
"github.com/hyperledger/firefly-common/pkg/config"
34+
"github.com/hyperledger/firefly-common/pkg/fftls"
3435
"github.com/hyperledger/firefly-common/pkg/httpserver"
3536
"github.com/hyperledger/firefly-common/pkg/i18n"
3637
"github.com/hyperledger/firefly-common/pkg/metric"
@@ -171,7 +172,8 @@ func buildPublicURL(conf config.Section, a net.Addr) string {
171172
publicURL := conf.GetString(httpserver.HTTPConfPublicURL)
172173
if publicURL == "" {
173174
proto := "https"
174-
if !conf.GetBool(httpserver.HTTPConfTLSEnabled) {
175+
tlsConfig := conf.SubSection("tls")
176+
if !tlsConfig.GetBool(fftls.HTTPConfTLSEnabled) {
175177
proto = "http"
176178
}
177179
publicURL = fmt.Sprintf("%s://%s", proto, a.String())

pkg/ffresty/config.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2022 Kaleido, Inc.
1+
// Copyright © 2023 Kaleido, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
//
@@ -16,7 +16,10 @@
1616

1717
package ffresty
1818

19-
import "github.com/hyperledger/firefly-common/pkg/config"
19+
import (
20+
"github.com/hyperledger/firefly-common/pkg/config"
21+
"github.com/hyperledger/firefly-common/pkg/fftls"
22+
)
2023

2124
const (
2225
defaultRetryEnabled = false
@@ -70,7 +73,7 @@ const (
7073
HTTPCustomClient = "customClient"
7174
)
7275

73-
func InitConfig(conf config.KeySet) {
76+
func InitConfig(conf config.Section) {
7477
conf.AddKnownKey(HTTPConfigURL)
7578
conf.AddKnownKey(HTTPConfigProxyURL)
7679
conf.AddKnownKey(HTTPConfigHeaders)
@@ -88,4 +91,7 @@ func InitConfig(conf config.KeySet) {
8891
conf.AddKnownKey(HTTPExpectContinueTimeout, defaultHTTPExpectContinueTimeout)
8992
conf.AddKnownKey(HTTPPassthroughHeadersEnabled, defaultHTTPPassthroughHeadersEnabled)
9093
conf.AddKnownKey(HTTPCustomClient)
94+
95+
tlsConfig := conf.SubSection("tls")
96+
fftls.InitTLSConfig(tlsConfig)
9197
}

pkg/ffresty/ffresty.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/go-resty/resty/v2"
3030
"github.com/hyperledger/firefly-common/pkg/config"
3131
"github.com/hyperledger/firefly-common/pkg/ffapi"
32+
"github.com/hyperledger/firefly-common/pkg/fftls"
3233
"github.com/hyperledger/firefly-common/pkg/fftypes"
3334
"github.com/hyperledger/firefly-common/pkg/i18n"
3435
"github.com/hyperledger/firefly-common/pkg/log"
@@ -67,10 +68,7 @@ func OnAfterResponse(c *resty.Client, resp *resty.Response) {
6768
//
6869
// You can use the normal Resty builder pattern, to set per-instance configuration
6970
// as required.
70-
func New(ctx context.Context, staticConfig config.Section) *resty.Client {
71-
72-
var client *resty.Client
73-
71+
func New(ctx context.Context, staticConfig config.Section) (client *resty.Client, err error) {
7472
passthroughHeadersEnabled := staticConfig.GetBool(HTTPPassthroughHeadersEnabled)
7573

7674
iHTTPClient := staticConfig.Get(HTTPCustomClient)
@@ -80,6 +78,7 @@ func New(ctx context.Context, staticConfig config.Section) *resty.Client {
8078
}
8179
}
8280
if client == nil {
81+
8382
httpTransport := &http.Transport{
8483
Proxy: http.ProxyFromEnvironment,
8584
DialContext: (&net.Dialer{
@@ -92,6 +91,14 @@ func New(ctx context.Context, staticConfig config.Section) *resty.Client {
9291
TLSHandshakeTimeout: staticConfig.GetDuration(HTTPTLSHandshakeTimeout),
9392
ExpectContinueTimeout: staticConfig.GetDuration(HTTPExpectContinueTimeout),
9493
}
94+
95+
tlsConfig, err := fftls.ConstructTLSConfig(ctx, staticConfig.SubSection("tls"), "client")
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
httpTransport.TLSClientConfig = tlsConfig
101+
95102
httpClient := &http.Client{
96103
Transport: httpTransport,
97104
}
@@ -184,7 +191,7 @@ func New(ctx context.Context, staticConfig config.Section) *resty.Client {
184191
})
185192
}
186193

187-
return client
194+
return client, nil
188195
}
189196

190197
func WrapRestErr(ctx context.Context, res *resty.Response, err error, key i18n.ErrorMessageKey) error {

pkg/ffresty/ffresty_test.go

Lines changed: 156 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,33 @@ package ffresty
1818

1919
import (
2020
"context"
21+
"crypto/rand"
22+
"crypto/rsa"
23+
"crypto/tls"
24+
"crypto/x509"
25+
"crypto/x509/pkix"
26+
"encoding/json"
27+
"encoding/pem"
2128
"fmt"
29+
"log"
30+
"math/big"
31+
"net"
2232
"net/http"
33+
"os"
2334
"strings"
2435
"testing"
36+
"time"
2537

2638
"github.com/hyperledger/firefly-common/pkg/config"
2739
"github.com/hyperledger/firefly-common/pkg/ffapi"
40+
"github.com/hyperledger/firefly-common/pkg/fftls"
2841
"github.com/hyperledger/firefly-common/pkg/i18n"
2942
"github.com/jarcoal/httpmock"
3043
"github.com/stretchr/testify/assert"
3144
)
3245

46+
const configDir = "../../test/data/config"
47+
3348
var utConf = config.RootSection("http_unit_tests")
3449

3550
func resetConf() {
@@ -51,7 +66,8 @@ func TestRequestOK(t *testing.T) {
5166
utConf.Set(HTTPConfigRetryEnabled, true)
5267
utConf.Set(HTTPCustomClient, customClient)
5368

54-
c := New(context.Background(), utConf)
69+
c, err := New(context.Background(), utConf)
70+
assert.Nil(t, err)
5571
httpmock.ActivateNonDefault(customClient)
5672
defer httpmock.DeactivateAndReset()
5773

@@ -79,7 +95,8 @@ func TestRequestRetry(t *testing.T) {
7995
utConf.Set(HTTPConfigRetryEnabled, true)
8096
utConf.Set(HTTPConfigRetryInitDelay, 1)
8197

82-
c := New(ctx, utConf)
98+
c, err := New(ctx, utConf)
99+
assert.Nil(t, err)
83100
httpmock.ActivateNonDefault(c.GetClient())
84101
defer httpmock.DeactivateAndReset()
85102

@@ -105,7 +122,8 @@ func TestConfWithProxy(t *testing.T) {
105122
utConf.Set(HTTPConfigProxyURL, "http://myproxy.example.com:12345")
106123
utConf.Set(HTTPConfigRetryEnabled, false)
107124

108-
c := New(ctx, utConf)
125+
c, err := New(ctx, utConf)
126+
assert.Nil(t, err)
109127
assert.True(t, c.IsProxySet())
110128
}
111129

@@ -117,7 +135,8 @@ func TestLongResponse(t *testing.T) {
117135
utConf.Set(HTTPConfigURL, "http://localhost:12345")
118136
utConf.Set(HTTPConfigRetryEnabled, false)
119137

120-
c := New(ctx, utConf)
138+
c, err := New(ctx, utConf)
139+
assert.Nil(t, err)
121140
httpmock.ActivateNonDefault(c.GetClient())
122141
defer httpmock.DeactivateAndReset()
123142

@@ -141,7 +160,8 @@ func TestErrResponse(t *testing.T) {
141160
utConf.Set(HTTPConfigURL, "http://localhost:12345")
142161
utConf.Set(HTTPConfigRetryEnabled, false)
143162

144-
c := New(ctx, utConf)
163+
c, err := New(ctx, utConf)
164+
assert.Nil(t, err)
145165
httpmock.ActivateNonDefault(c.GetClient())
146166
defer httpmock.DeactivateAndReset()
147167

@@ -180,7 +200,8 @@ func TestPassthroughHeaders(t *testing.T) {
180200
utConf.Set(HTTPCustomClient, customClient)
181201
utConf.Set(HTTPPassthroughHeadersEnabled, true)
182202

183-
c := New(context.Background(), utConf)
203+
c, err := New(context.Background(), utConf)
204+
assert.Nil(t, err)
184205
httpmock.ActivateNonDefault(customClient)
185206
defer httpmock.DeactivateAndReset()
186207

@@ -200,3 +221,132 @@ func TestPassthroughHeaders(t *testing.T) {
200221

201222
assert.Equal(t, 1, httpmock.GetTotalCallCount())
202223
}
224+
225+
func TestMissingCAFile(t *testing.T) {
226+
resetConf()
227+
utConf.Set(HTTPConfigURL, "https://localhost:12345")
228+
tlsSection := utConf.SubSection("tls")
229+
tlsSection.Set(fftls.HTTPConfTLSEnabled, true)
230+
tlsSection.Set(fftls.HTTPConfTLSCAFile, "non-existent.pem")
231+
232+
_, err := New(context.Background(), utConf)
233+
assert.Regexp(t, "FF00153", err)
234+
}
235+
236+
func TestBadCAFile(t *testing.T) {
237+
resetConf()
238+
utConf.Set(HTTPConfigURL, "https://localhost:12345")
239+
tlsSection := utConf.SubSection("tls")
240+
tlsSection.Set(fftls.HTTPConfTLSEnabled, true)
241+
tlsSection.Set(fftls.HTTPConfTLSCAFile, configDir+"/firefly.common.yaml")
242+
243+
_, err := New(context.Background(), utConf)
244+
assert.Regexp(t, "FF00152", err)
245+
}
246+
247+
func TestBadKeyPair(t *testing.T) {
248+
resetConf()
249+
utConf.Set(HTTPConfigURL, "https://localhost:12345")
250+
tlsSection := utConf.SubSection("tls")
251+
tlsSection.Set(fftls.HTTPConfTLSEnabled, true)
252+
tlsSection.Set(fftls.HTTPConfTLSCertFile, configDir+"/firefly.common.yaml")
253+
tlsSection.Set(fftls.HTTPConfTLSKeyFile, configDir+"/firefly.common.yaml")
254+
255+
_, err := New(context.Background(), utConf)
256+
assert.Regexp(t, "FF00206", err)
257+
}
258+
259+
func TestMTLSClientWithServer(t *testing.T) {
260+
// Create an X509 certificate pair
261+
privatekey, _ := rsa.GenerateKey(rand.Reader, 2048)
262+
publickey := &privatekey.PublicKey
263+
var privateKeyBytes []byte = x509.MarshalPKCS1PrivateKey(privatekey)
264+
privateKeyFile, _ := os.CreateTemp("", "key.pem")
265+
defer os.Remove(privateKeyFile.Name())
266+
privateKeyBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: privateKeyBytes}
267+
pem.Encode(privateKeyFile, privateKeyBlock)
268+
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
269+
x509Template := &x509.Certificate{
270+
SerialNumber: serialNumber,
271+
Subject: pkix.Name{
272+
Organization: []string{"Unit Tests"},
273+
},
274+
NotBefore: time.Now(),
275+
NotAfter: time.Now().Add(100 * time.Second),
276+
KeyUsage: x509.KeyUsageDigitalSignature,
277+
BasicConstraintsValid: true,
278+
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)},
279+
}
280+
derBytes, err := x509.CreateCertificate(rand.Reader, x509Template, x509Template, publickey, privatekey)
281+
assert.NoError(t, err)
282+
publicKeyFile, _ := os.CreateTemp("", "cert.pem")
283+
defer os.Remove(publicKeyFile.Name())
284+
pem.Encode(publicKeyFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
285+
286+
http.HandleFunc("/hello", func(res http.ResponseWriter, req *http.Request) {
287+
res.WriteHeader(200)
288+
json.NewEncoder(res).Encode(map[string]interface{}{"hello": "world"})
289+
})
290+
291+
// Create a CA certificate pool and add cert.pem to it
292+
caCert, err := os.ReadFile(publicKeyFile.Name())
293+
if err != nil {
294+
log.Fatal(err)
295+
}
296+
caCertPool := x509.NewCertPool()
297+
caCertPool.AppendCertsFromPEM(caCert)
298+
299+
// Create the TLS Config with the CA pool and enable Client certificate validation
300+
tlsConfig := &tls.Config{
301+
ClientCAs: caCertPool,
302+
ClientAuth: tls.RequireAndVerifyClientCert,
303+
}
304+
tlsConfig.BuildNameToCertificate()
305+
306+
// Create a Server instance to listen on port 8443 with the TLS config
307+
server := &http.Server{
308+
Addr: "127.0.0.1:8443",
309+
TLSConfig: tlsConfig,
310+
}
311+
312+
ctx, cancelCtx := context.WithCancel(context.Background())
313+
go func() {
314+
select {
315+
case <-ctx.Done():
316+
shutdownContext, cancel := context.WithTimeout(context.Background(), 2*time.Second)
317+
defer cancel()
318+
if err := server.Shutdown(shutdownContext); err != nil {
319+
return
320+
}
321+
}
322+
}()
323+
324+
go server.ListenAndServeTLS(publicKeyFile.Name(), privateKeyFile.Name())
325+
326+
// Use ffresty to test the mTLS client as well
327+
var restyConfig = config.RootSection("resty")
328+
InitConfig(restyConfig)
329+
clientTLSSection := restyConfig.SubSection("tls")
330+
restyConfig.Set(HTTPConfigURL, "https://127.0.0.1")
331+
clientTLSSection.Set(fftls.HTTPConfTLSEnabled, true)
332+
clientTLSSection.Set(fftls.HTTPConfTLSKeyFile, privateKeyFile.Name())
333+
clientTLSSection.Set(fftls.HTTPConfTLSCertFile, publicKeyFile.Name())
334+
clientTLSSection.Set(fftls.HTTPConfTLSCAFile, publicKeyFile.Name())
335+
336+
c, err := New(context.Background(), restyConfig)
337+
assert.Nil(t, err)
338+
339+
//httpsAddr := fmt.Sprintf("https://localhost:8443/hello", server.Addr)
340+
res, err := c.R().Get("https://127.0.0.1:8443/hello")
341+
assert.NoError(t, err)
342+
343+
assert.NoError(t, err)
344+
if res != nil {
345+
assert.Equal(t, 200, res.StatusCode())
346+
var resBody map[string]interface{}
347+
err = json.Unmarshal(res.Body(), &resBody)
348+
assert.NoError(t, err)
349+
assert.Equal(t, "world", resBody["hello"])
350+
}
351+
cancelCtx()
352+
}

0 commit comments

Comments
 (0)