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  }