github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/pkg/cmd/server_test.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 package cmd 18 19 import ( 20 "bytes" 21 "context" 22 "io" 23 "net/http" 24 "net/url" 25 "os" 26 "path/filepath" 27 "sync" 28 "testing" 29 "time" 30 31 api "github.com/freiheit-com/kuberpult/pkg/api/v1" 32 "github.com/google/go-cmp/cmp" 33 "google.golang.org/protobuf/proto" 34 ) 35 36 func TestServerHeader(t *testing.T) { 37 tcs := []struct { 38 Name string 39 RequestPath string 40 RequestMethod string 41 RequestHeaders http.Header 42 Environment map[string]string 43 44 ExpectedHeaders http.Header 45 }{ 46 { 47 Name: "simple case", 48 RequestPath: "/", 49 50 ExpectedHeaders: http.Header{ 51 "Accept-Ranges": {"bytes"}, 52 "Content-Type": {"text/html; charset=utf-8"}, 53 "Content-Security-Policy": { 54 "default-src 'self'; style-src-elem 'self' fonts.googleapis.com 'unsafe-inline'; font-src fonts.gstatic.com; connect-src 'self' login.microsoftonline.com; child-src 'none'", 55 }, 56 "Permission-Policy": { 57 "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=()", 58 }, 59 "Referrer-Policy": {"no-referrer"}, 60 "Strict-Transport-Security": {"max-age=31536000; includeSubDomains;"}, 61 "X-Content-Type-Options": {"nosniff"}, 62 "X-Frame-Options": {"DENY"}, 63 }, 64 }, 65 { 66 67 Name: "cors", 68 RequestMethod: "OPTIONS", 69 RequestHeaders: http.Header{ 70 "Origin": {"https://something.else"}, 71 }, 72 Environment: map[string]string{ 73 "KUBERPULT_ALLOWED_ORIGINS": "https://kuberpult.fdc", 74 }, 75 76 ExpectedHeaders: http.Header{ 77 "Accept-Ranges": {"bytes"}, 78 "Access-Control-Allow-Credentials": {"true"}, 79 "Access-Control-Allow-Origin": {"https://kuberpult.fdc"}, 80 "Content-Type": {"text/html; charset=utf-8"}, 81 "Content-Security-Policy": {"default-src 'self'; style-src-elem 'self' fonts.googleapis.com 'unsafe-inline'; font-src fonts.gstatic.com; connect-src 'self' login.microsoftonline.com; child-src 'none'"}, 82 83 "Permission-Policy": { 84 "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=()", 85 }, 86 "Referrer-Policy": {"no-referrer"}, 87 "Strict-Transport-Security": {"max-age=31536000; includeSubDomains;"}, 88 "X-Content-Type-Options": {"nosniff"}, 89 "X-Frame-Options": {"DENY"}, 90 }, 91 }, 92 { 93 94 Name: "cors preflight", 95 RequestMethod: "OPTIONS", 96 RequestHeaders: http.Header{ 97 "Origin": {"https://something.else"}, 98 "Access-Control-Request-Method": {"POST"}, 99 }, 100 Environment: map[string]string{ 101 "KUBERPULT_ALLOWED_ORIGINS": "https://kuberpult.fdc", 102 }, 103 104 ExpectedHeaders: http.Header{ 105 "Access-Control-Allow-Credentials": {"true"}, 106 "Access-Control-Allow-Headers": {"content-type,x-grpc-web,authorization"}, 107 "Access-Control-Allow-Methods": {"POST"}, 108 "Access-Control-Allow-Origin": {"https://kuberpult.fdc"}, 109 "Access-Control-Max-Age": {"0"}, 110 }, 111 }, 112 } 113 for _, tc := range tcs { 114 tc := tc 115 t.Run(tc.Name, func(t *testing.T) { 116 var wg sync.WaitGroup 117 ctx, cancel := context.WithCancel(context.Background()) 118 wg.Add(1) 119 go func(t *testing.T) { 120 defer wg.Done() 121 defer cancel() 122 for { 123 res, err := http.Get("http://localhost:8081/healthz") 124 if err != nil { 125 t.Logf("unhealthy: %q", err) 126 <-time.After(1 * time.Second) 127 continue 128 } 129 if res.StatusCode != 200 { 130 t.Logf("status: %q", res.StatusCode) 131 <-time.After(1 * time.Second) 132 continue 133 } 134 break 135 } 136 // 137 path, err := url.JoinPath("http://localhost:8081/", tc.RequestPath) 138 if err != nil { 139 panic(err) 140 } 141 req, err := http.NewRequest(tc.RequestMethod, path, nil) 142 if err != nil { 143 t.Fatalf("expected no error but got %q", err) 144 } 145 req.Header = tc.RequestHeaders 146 res, err := http.DefaultClient.Do(req) 147 if err != nil { 148 t.Fatalf("expected no error but got %q", err) 149 } 150 t.Logf("%v %q", res.StatusCode, err) 151 // Delete three headers that are hard to test. 152 hdrs := res.Header.Clone() 153 hdrs.Del("Content-Length") 154 hdrs.Del("Date") 155 hdrs.Del("Last-Modified") 156 hdrs.Del("Cache-Control") // for caching tests see TestServeHttpBasics 157 body, _ := io.ReadAll(res.Body) 158 t.Logf("body: %q", body) 159 if !cmp.Equal(tc.ExpectedHeaders, hdrs) { 160 t.Errorf("expected no diff for headers but got %s", cmp.Diff(tc.ExpectedHeaders, hdrs)) 161 } 162 163 }(t) 164 for k, v := range tc.Environment { 165 t.Setenv(k, v) 166 } 167 td := t.TempDir() 168 err := os.Mkdir(filepath.Join(td, "build"), 0755) 169 if err != nil { 170 t.Fatal(err) 171 } 172 err = os.WriteFile(filepath.Join(td, "build", "index.html"), ([]byte)(`<!doctype html><html lang="en"></html>`), 0755) 173 if err != nil { 174 t.Fatal(err) 175 } 176 err = os.Chdir(td) 177 if err != nil { 178 t.Fatal(err) 179 } 180 err = os.Setenv("KUBERPULT_GIT_AUTHOR_EMAIL", "mail2") 181 if err != nil { 182 t.Fatalf("expected no error, but got %q", err) 183 } 184 err = os.Setenv("KUBERPULT_GIT_AUTHOR_NAME", "name1") 185 if err != nil { 186 t.Fatalf("expected no error, but got %q", err) 187 } 188 err = runServer(ctx) 189 if err != nil { 190 t.Fatalf("expected no error, but got %q", err) 191 } 192 wg.Wait() 193 }) 194 } 195 } 196 197 func TestGrpcForwardHeader(t *testing.T) { 198 tcs := []struct { 199 Name string 200 Environment map[string]string 201 202 RequestPath string 203 Body proto.Message 204 205 ExpectedHttpStatusCode int 206 }{ 207 { 208 Name: "rollout server unimplemented", 209 RequestPath: "/api.v1.RolloutService/StreamStatus", 210 Body: &api.StreamStatusRequest{}, 211 ExpectedHttpStatusCode: 200, 212 }, 213 } 214 for _, tc := range tcs { 215 tc := tc 216 t.Run(tc.Name, func(t *testing.T) { 217 var wg sync.WaitGroup 218 ctx, cancel := context.WithCancel(context.Background()) 219 wg.Add(1) 220 go func(t *testing.T) { 221 defer wg.Done() 222 defer cancel() 223 for { 224 res, err := http.Get("http://localhost:8081/healthz") 225 if err != nil { 226 t.Logf("unhealthy: %q", err) 227 <-time.After(1 * time.Second) 228 continue 229 } 230 if res.StatusCode != 200 { 231 t.Logf("status: %q", res.StatusCode) 232 <-time.After(1 * time.Second) 233 continue 234 } 235 break 236 } 237 path, err := url.JoinPath("http://localhost:8081/", tc.RequestPath) 238 if err != nil { 239 t.Fatalf("error joining url: %s", err) 240 } 241 body, err := proto.Marshal(tc.Body) 242 req, err := http.NewRequest("POST", path, bytes.NewReader(body)) 243 if err != nil { 244 t.Fatalf("expected no error but got %q", err) 245 } 246 req.Header.Add("Content-Type", "application/grpc-web") 247 res, err := http.DefaultClient.Do(req) 248 if err != nil { 249 t.Fatalf("expected no error but got %q", err) 250 } 251 _, _ = io.ReadAll(res.Body) 252 if tc.ExpectedHttpStatusCode != res.StatusCode { 253 t.Errorf("unexpected http status code, expected %d, got %d", tc.ExpectedHttpStatusCode, res.StatusCode) 254 } 255 // TODO(HVG): test the grpc status 256 }(t) 257 for k, v := range tc.Environment { 258 t.Setenv(k, v) 259 } 260 err := os.Setenv("KUBERPULT_GIT_AUTHOR_EMAIL", "mail2") 261 if err != nil { 262 t.Fatalf("expected no error, but got %q", err) 263 } 264 err = os.Setenv("KUBERPULT_GIT_AUTHOR_NAME", "name1") 265 if err != nil { 266 t.Fatalf("expected no error, but got %q", err) 267 } 268 t.Logf("env var: %s", os.Getenv("KUBERPULT_GIT_AUTHOR_EMAIL")) 269 err = runServer(ctx) 270 if err != nil { 271 t.Fatalf("expected no error, but got %q", err) 272 } 273 wg.Wait() 274 }) 275 } 276 }