github.com/splucs/witchcraft-go-server@v1.7.0/integration/server_test.go (about) 1 // Copyright (c) 2018 Palantir Technologies. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package integration 16 17 import ( 18 "bytes" 19 "context" 20 "crypto/tls" 21 "encoding/json" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 "os" 26 "path" 27 "testing" 28 "time" 29 30 "github.com/nmiyake/pkg/dirs" 31 "github.com/palantir/pkg/httpserver" 32 "github.com/palantir/pkg/tlsconfig" 33 "github.com/palantir/witchcraft-go-server/config" 34 "github.com/palantir/witchcraft-go-server/conjure/witchcraft/api/health" 35 "github.com/palantir/witchcraft-go-server/status" 36 "github.com/palantir/witchcraft-go-server/witchcraft" 37 "github.com/stretchr/testify/assert" 38 "github.com/stretchr/testify/require" 39 ) 40 41 // TestServerStarts ensures that a Witchcraft server starts and is able to serve requests. 42 // It also verifies the service log output of starting two routers (application and management). 43 func TestServerStarts(t *testing.T) { 44 logOutputBuffer := &bytes.Buffer{} 45 server, _, _, _, cleanup := createAndRunTestServer(t, nil, logOutputBuffer) 46 defer func() { 47 _ = server.Close() 48 }() 49 defer cleanup() 50 51 // verify service log output 52 msgs := getLogFileMessages(t, logOutputBuffer.Bytes()) 53 assert.Equal(t, []string{"Listening to https", "Listening to https"}, msgs) 54 } 55 56 // TestServerShutdown verifies the behavior when shutting down a Witchcraft server. There are two variants, graceful and abrupt. 57 // Graceful: server.Shutdown, which allows in-flight requests to complete. We test this using a route which sleeps one second. 58 // Abrupt: server.Close, which terminates the server immediately and sends EOF on active connections. 59 func TestServerShutdown(t *testing.T) { 60 runTest := func(t *testing.T, graceful bool) { 61 logOutputBuffer := &bytes.Buffer{} 62 calledC := make(chan bool, 1) 63 doneC := make(chan bool, 1) 64 initFn := func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 65 return nil, info.Router.Get("/wait", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 66 calledC <- true 67 // just wait for 1 second to hold the connection open. 68 time.Sleep(1 * time.Second) 69 doneC <- true 70 })) 71 } 72 server, port, _, serverErr, cleanup := createAndRunTestServer(t, initFn, logOutputBuffer) 73 defer func() { 74 _ = server.Close() 75 }() 76 defer cleanup() 77 78 go func() { 79 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/example/wait", port)) 80 if graceful { 81 if err != nil { 82 panic(fmt.Errorf("graceful shutdown request failed: %v", err)) 83 } 84 if resp == nil { 85 panic(fmt.Errorf("returned response was nil")) 86 } 87 if resp.StatusCode != 200 { 88 panic(fmt.Errorf("server did not respond with 200 OK. Got %s", resp.Status)) 89 } 90 } else { 91 if err == nil { 92 panic(fmt.Errorf("request allowed to finish successfully")) 93 } 94 } 95 }() 96 97 timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), time.Second) 98 defer timeoutCancel() 99 100 select { 101 case <-calledC: 102 case <-timeoutCtx.Done(): 103 require.NoError(t, timeoutCtx.Err(), "timed out waiting for called") 104 } 105 106 select { 107 case done := <-doneC: 108 require.False(t, done, "Connection was already closed!") 109 default: 110 } 111 112 if graceful { 113 // Gracefully shut down server. This should block until the handler has completed. 114 require.NoError(t, server.Shutdown(context.Background())) 115 } else { 116 // Abruptly close server. This will send EOF on open connections. 117 require.NoError(t, server.Close()) 118 } 119 120 var done bool 121 select { 122 case done = <-doneC: 123 default: 124 } 125 126 if graceful { 127 require.True(t, done, "Handler didn't execute the whole way!") 128 129 // verify service log output 130 msgs := getLogFileMessages(t, logOutputBuffer.Bytes()) 131 assert.Equal(t, []string{"Listening to https", "Listening to https", "Shutting down server", "example was closed", "example-management was closed"}, msgs) 132 } else { 133 require.False(t, done, "Handler allowed to execute the whole way") 134 } 135 136 select { 137 case err := <-serverErr: 138 require.NoError(t, err) 139 default: 140 } 141 } 142 143 t.Run("graceful shutdown", func(t *testing.T) { 144 runTest(t, true) 145 }) 146 t.Run("abrupt shutdown", func(t *testing.T) { 147 runTest(t, false) 148 }) 149 } 150 151 // TestEmptyPathHandler verifies that a route registered at the default path ("/") is served correctly. 152 func TestEmptyPathHandler(t *testing.T) { 153 logOutputBuffer := &bytes.Buffer{} 154 var called bool 155 server, port, _, serverErr, cleanup := createAndRunTestServer(t, func(ctx context.Context, info witchcraft.InitInfo) (deferFn func(), rErr error) { 156 return nil, info.Router.Get("/", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 157 called = true 158 })) 159 }, logOutputBuffer) 160 defer func() { 161 _ = server.Close() 162 }() 163 defer cleanup() 164 165 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/%s", port, basePath)) 166 require.NoError(t, err) 167 require.NotNil(t, resp) 168 require.Equal(t, "200 OK", resp.Status) 169 assert.True(t, called, "called boolean was not set to true (http handler did not execute)") 170 171 // verify service log output 172 msgs := getLogFileMessages(t, logOutputBuffer.Bytes()) 173 assert.Equal(t, []string{"Listening to https", "Listening to https"}, msgs) 174 175 select { 176 case err := <-serverErr: 177 require.NoError(t, err) 178 default: 179 } 180 } 181 182 // TestManagementRoutes verifies the behavior of /liveness, /readiness, and /health endpoints in their default configuration. 183 // There are two variants - one with a dedicated management port/router and one using the application port/router. 184 func TestManagementRoutes(t *testing.T) { 185 runTests := func(t *testing.T, mgmtPort int) { 186 t.Run("Liveness", func(t *testing.T) { 187 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/example/%s", mgmtPort, status.LivenessEndpoint)) 188 require.NoError(t, err) 189 require.NotNil(t, resp) 190 require.Equal(t, "200 OK", resp.Status) 191 }) 192 t.Run("Readiness", func(t *testing.T) { 193 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/example/%s", mgmtPort, status.ReadinessEndpoint)) 194 require.NoError(t, err) 195 require.NotNil(t, resp) 196 require.Equal(t, "200 OK", resp.Status) 197 }) 198 t.Run("Health", func(t *testing.T) { 199 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/example/%s", mgmtPort, status.HealthEndpoint)) 200 require.NoError(t, err) 201 require.NotNil(t, resp) 202 require.Equal(t, "200 OK", resp.Status) 203 body, err := ioutil.ReadAll(resp.Body) 204 require.NoError(t, err) 205 require.NoError(t, resp.Body.Close()) 206 var healthResp health.HealthStatus 207 require.NoError(t, json.Unmarshal(body, &healthResp)) 208 require.Equal(t, health.HealthStatus{Checks: map[health.CheckType]health.HealthCheckResult{ 209 "SERVER_STATUS": {Type: "SERVER_STATUS", State: health.HealthStateHealthy, Params: map[string]interface{}{}}, 210 }}, healthResp) 211 }) 212 } 213 214 t.Run("dedicated port", func(t *testing.T) { 215 server, _, managementPort, serverErr, cleanup := createAndRunTestServer(t, nil, ioutil.Discard) 216 defer func() { 217 _ = server.Close() 218 }() 219 defer cleanup() 220 221 runTests(t, managementPort) 222 223 select { 224 case err := <-serverErr: 225 require.NoError(t, err) 226 default: 227 } 228 }) 229 230 t.Run("same port", func(t *testing.T) { 231 port, err := httpserver.AvailablePort() 232 require.NoError(t, err) 233 server, serverErr, cleanup := createAndRunCustomTestServer(t, port, port, nil, ioutil.Discard, createTestServer) 234 235 defer func() { 236 _ = server.Close() 237 }() 238 defer cleanup() 239 240 runTests(t, port) 241 242 select { 243 case err := <-serverErr: 244 require.NoError(t, err) 245 default: 246 } 247 }) 248 } 249 250 // TestClientTLS verifies that a Witchcraft server configured to require client TLS authentication enforces that config. 251 func TestClientTLS(t *testing.T) { 252 testDir, cleanup, err := dirs.TempDir("", "") 253 require.NoError(t, err) 254 defer cleanup() 255 256 wd, err := os.Getwd() 257 require.NoError(t, err) 258 defer func() { 259 err := os.Chdir(wd) 260 require.NoError(t, err) 261 }() 262 263 err = os.Chdir(testDir) 264 require.NoError(t, err) 265 266 port, err := httpserver.AvailablePort() 267 require.NoError(t, err) 268 managementPort, err := httpserver.AvailablePort() 269 require.NoError(t, err) 270 271 server := witchcraft.NewServer(). 272 WithClientAuth(tls.RequireAndVerifyClientCert). 273 WithECVKeyProvider(witchcraft.ECVKeyNoOp()). 274 WithRuntimeConfig(struct{}{}). 275 WithInstallConfig(config.Install{ 276 ProductName: productName, 277 UseConsoleLog: true, 278 Server: config.Server{ 279 Address: "localhost", 280 Port: port, 281 ManagementPort: managementPort, 282 ContextPath: basePath, 283 CertFile: path.Join(wd, "testdata/server-cert.pem"), 284 KeyFile: path.Join(wd, "testdata/server-key.pem"), 285 ClientCAFiles: []string{path.Join(wd, "testdata/ca-cert.pem")}, 286 }, 287 }) 288 289 serverChan := make(chan error) 290 go func() { 291 serverChan <- server.Start() 292 }() 293 294 select { 295 case err := <-serverChan: 296 require.NoError(t, err) 297 default: 298 } 299 300 // Management port should not require client certs. 301 ready := <-waitForTestServerReady(managementPort, path.Join(basePath, status.LivenessEndpoint), 5*time.Second) 302 if !ready { 303 errMsg := "timed out waiting for server to start. This could mean management server incorrectly requested client certs." 304 select { 305 case err := <-serverChan: 306 errMsg = fmt.Sprintf("%s: %+v", errMsg, err) 307 } 308 require.Fail(t, errMsg) 309 } 310 require.True(t, ready) 311 312 defer func() { 313 require.NoError(t, server.Close()) 314 }() 315 316 // Assert regular client receives error 317 _, err = testServerClient().Get(fmt.Sprintf("https://localhost:%d/example", port)) 318 require.Error(t, err, "Client allowed to make request without cert") 319 assert.Contains(t, err.Error(), "tls: bad certificate") 320 321 // Assert client w/ certs does not receive error 322 tlsConf, err := tlsconfig.NewClientConfig(tlsconfig.ClientRootCAFiles(path.Join(wd, "testdata/ca-cert.pem")), tlsconfig.ClientKeyPairFiles(path.Join(wd, "testdata/client-cert.pem"), path.Join(wd, "testdata/client-key.pem"))) 323 require.NoError(t, err) 324 _, err = (&http.Client{Transport: &http.Transport{TLSClientConfig: tlsConf}}).Get(fmt.Sprintf("https://localhost:%d/example", port)) 325 require.NoError(t, err) 326 }