From 320d93c34f019ffda8b07c3d0328cf0c0485ab54 Mon Sep 17 00:00:00 2001 From: Nicolas Pernoud <github@ninico.fr> Date: Wed, 30 Jun 2021 11:17:22 +0200 Subject: [PATCH] feat: completed tests on rootmux --- internal/franceconnect/mock.go | 11 +++- internal/matcher/matcher.go | 15 +++-- internal/matcher/matcher_test.go | 6 +- internal/rootmux/rootmux_test.go | 99 ++++++++++++++++++++++++++++++-- pkg/middlewares/middlewares.go | 3 +- 5 files changed, 118 insertions(+), 16 deletions(-) mode change 100644 => 100755 internal/franceconnect/mock.go mode change 100644 => 100755 internal/matcher/matcher.go mode change 100644 => 100755 internal/matcher/matcher_test.go mode change 100644 => 100755 internal/rootmux/rootmux_test.go diff --git a/internal/franceconnect/mock.go b/internal/franceconnect/mock.go old mode 100644 new mode 100755 index fc5770d..4881d39 --- a/internal/franceconnect/mock.go +++ b/internal/franceconnect/mock.go @@ -15,7 +15,12 @@ func CreateMock() *http.ServeMux { redir := strings.Replace(query.Get("redirect_uri"), "/callback", "/api/oidc/callback", 1) + "?state=" + query.Get("state") + "&code=mock_code" http.Redirect(w, r, redir, http.StatusFound) }) - + // Returns authorization code back to the user for matcher use case + mux.HandleFunc("/auth2", func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + redir := strings.Replace(query.Get("redirect_uri"), "/callback", "/api/matcher/callback", 1) + "?state=" + query.Get("state") + "&code=mock_code" + http.Redirect(w, r, redir, http.StatusFound) + }) // Returns access token back to the user mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -26,6 +31,10 @@ func CreateMock() *http.ServeMux { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"given_name":"Angela Claire Louise","family_name":"DUBOIS","birthdate":"1962-08-24","gender":"female","birthplace":"75107","birthcountry":"99100","preferred_username":"","sub":"b6048e95bb134ec5b1d1e1fa69f287172e91722b9354d637a1bcf2ebb0fd2ef5v1"}`)) }) + mux.HandleFunc("/userinfo2", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"given_name":"Paul Louis","family_name":"DUPONT","birthdate":"1962-08-24","gender":"male","birthplace":"75107","birthcountry":"99100","preferred_username":"","sub":"dcc2d409424c519ae0599c8585b585711020bd4035b91633e9eabaa9b7542721v1"}`)) + }) // Logout mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Logout OK") diff --git a/internal/matcher/matcher.go b/internal/matcher/matcher.go old mode 100644 new mode 100755 index 8dd7b24..d5166a0 --- a/internal/matcher/matcher.go +++ b/internal/matcher/matcher.go @@ -65,7 +65,7 @@ func CreateMatcherServer() *http.ServeMux { } // Send a mail to the CEO with the aforementionned token - err = sendMailToMandater(string(email), rd.Id.GivenName+" "+rd.Id.FamilyName, rd.Sirent, token, r.Host) + err = sendMailToMandater(string(email), rd.Id.GivenName+" "+rd.Id.FamilyName, rd.Sirent, token, common.GetDomain(r)) if err != nil { http.Error(w, "could not send email", http.StatusBadRequest) return @@ -80,10 +80,10 @@ func CreateMatcherServer() *http.ServeMux { tokens.ExtractAndValidateToken(r, "", &md, false) // Store the demand in a cookie - tokens.CreateCookie(md, r.Host, "MandateDemand", 60*time.Second, w) + tokens.CreateCookie(md, common.GetDomain(r), "MandateDemand", 60*time.Second, w) // Set a cookie to redirect to the needed callback route from the front end generic callback - cookie := http.Cookie{Name: "callbackRoute", Domain: r.Host, Path: "/", Value: "/api/matcher/callback", MaxAge: 60, Secure: false, HttpOnly: false, SameSite: http.SameSiteLaxMode} + cookie := http.Cookie{Name: "callbackRoute", Domain: common.GetDomain(r), Path: "/", Value: "/api/matcher/callback", MaxAge: 60, Secure: false, HttpOnly: false, SameSite: http.SameSiteLaxMode} http.SetCookie(w, &cookie) // Perform an France Connect authentication ... and wait for the callback @@ -126,7 +126,6 @@ func CreateMatcherServer() *http.ServeMux { return } w.Write([]byte("the mandate could not be created, you do not seem to be the company CEO")) - return // TODO : Inform the original asker that is demand has been validated/rejected }) @@ -170,3 +169,11 @@ func sendMailToMandater(email string, mandate string, sirent string, token strin // Sending email. return mailSender.Send(to, body.Bytes()) } + +// SetTestMailSender allows overriding mail sender for test purposes +func SetTestModeAndReturnRecorder() *email.EmailRecorder { + mailTemplate = "../../mailtemplate.html" + sender, recorder := email.NewMockSender() + mailSender = sender + return recorder +} diff --git a/internal/matcher/matcher_test.go b/internal/matcher/matcher_test.go old mode 100644 new mode 100755 index 5a9371e..c19f93c --- a/internal/matcher/matcher_test.go +++ b/internal/matcher/matcher_test.go @@ -3,14 +3,10 @@ package matcher import ( "strings" "testing" - - "forge.grandlyon.com/npernoud/glcpro/pkg/email" ) func Test_sendMailToMandater(t *testing.T) { - mailTemplate = "../../mailtemplate.html" - sender, recorder := email.NewMockSender() - mailSender = sender + recorder := SetTestModeAndReturnRecorder() type args struct { email string mandate string diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go old mode 100644 new mode 100755 index 771c60b..5970056 --- a/internal/rootmux/rootmux_test.go +++ b/internal/rootmux/rootmux_test.go @@ -2,6 +2,7 @@ package rootmux import ( "encoding/base64" + "fmt" "net/http/cookiejar" "net/http/httptest" "net/url" @@ -13,17 +14,21 @@ import ( "forge.grandlyon.com/npernoud/glcpro/internal/apientreprise" "forge.grandlyon.com/npernoud/glcpro/internal/franceconnect" + "forge.grandlyon.com/npernoud/glcpro/internal/matcher" "forge.grandlyon.com/npernoud/glcpro/pkg/tester" "forge.grandlyon.com/npernoud/glcpro/pkg/tokens" ) var ( - noH map[string]string + noH map[string]string + fcServer *httptest.Server + clientRedirectURI = "http://client.org" + tokenMustContent = `"id_token":{"given_name":"Angela` ) func TestMain(m *testing.M) { // Create the france connect mock server - fcServer := httptest.NewServer(franceconnect.CreateMock()) + fcServer = httptest.NewServer(franceconnect.CreateMock()) defer fcServer.Close() // Setup to use the france connect mock server os.Setenv("FC_AUTH", fcServer.URL+"/auth") @@ -37,6 +42,8 @@ func TestMain(m *testing.M) { apientreprise.Init("../../configs/apicache.json") code := m.Run() + // Remove the database + os.Remove("./glcpro.db") os.Exit(code) } @@ -55,18 +62,22 @@ func createTester(t *testing.T) (*httptest.Server, tester.DoFn, tester.DoFn) { // Test the use case 1 : "Cas d'usage 1 : Accès à une démarche depuis un portail de service public pour un dirigeant" func TestUseCase1(t *testing.T) { - clientRedirectURI := "http://client.org" - // Create the tester _, do, _ := createTester(t) // (We arrive from a client with a query, and the front end add the requested SIRENT to the query), we should be redirected to france connect, login, and be back with an authorisation code r := do("GET", "/api/oidc/auth?scope=openid%20profile&client_id=A_RANDOM_ID&redirect_uri="+clientRedirectURI+"&response_type=code&state=A_RANDOM_STATE&sirent=000000001", noH, "", 302, "") + // We are redirected to France Connect r = redirectURIFromBody(r) + fmt.Printf("Redirected to France Connect : %v\n", r) r = do("GET", r, noH, "", 302, "") + // We are redirected to GLC Pro (France Connect callback) r = redirectURIFromBody(r) + fmt.Printf("Redirected to GLC Pro callback : %v\n", r) r = do("GET", r, noH, "", 302, "") + // We are redirected to the client r = redirectURIFromBody(r) + fmt.Printf("Redirected to Client : %v\n", r) if !strings.Contains(r, clientRedirectURI) { t.Errorf("no redirection to the client") } @@ -75,9 +86,87 @@ func TestUseCase1(t *testing.T) { tk := do("POST", "/api/oidc/token", noH, "client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=authorization_code&code="+code, 200, "") tk = regexp.MustCompile(`id_token=(.*)&scope.*`).FindStringSubmatch(tk)[1] token, _ := base64.StdEncoding.DecodeString(tk) - if !strings.Contains(string(token), "Angela Claire Louise") || !strings.Contains(string(token), "THE TEST COMPANY") { + if !strings.Contains(string(token), tokenMustContent) || !strings.Contains(string(token), "THE TEST COMPANY") { t.Errorf("id token is not complete") } + fmt.Printf("Token : %v\n", string(token)) +} + +// Test the use case 2 : "Cas d'usage 2 : Demande d'habilitation par un dirigeant" +func TestUseCase2(t *testing.T) { + + // Create the tester + _, do, _ := createTester(t) + + // Create the mock mail server + recorder := matcher.SetTestModeAndReturnRecorder() + + ///////////////////////////////////// + // THE MANDATEE MAKES THE DEMAND // + ///////////////////////////////////// + + // (We arrive from a client with a query, and the front end add the requested SIRENT to the query), we should be redirected to france connect, login, .. and do NOT get a code but be redirect on the mandate demand page + // Configure FC Mock to give the ID of Paul Louis Dupont + os.Setenv("FC_USER_INFO", fcServer.URL+"/userinfo2") + franceconnect.Init() + r := do("GET", "/api/oidc/auth?scope=openid%20profile&client_id=A_RANDOM_ID&redirect_uri="+clientRedirectURI+"&response_type=code&state=A_RANDOM_STATE&sirent=000000001", noH, "", 302, "") + // We are redirected to France Connect + r = redirectURIFromBody(r) + fmt.Printf("Redirected to France Connect : %v\n", r) + r = do("GET", r, noH, "", 302, "") + // We are redirected to GLC Pro (France Connect callback) + r = redirectURIFromBody(r) + fmt.Printf("Redirected to GLC Pro callback : %v\n", r) + r = do("GET", r, noH, "", 302, "") + // We are redirected to the matcher + r = redirectURIFromBody(r) + fmt.Printf("Redirected to GLC Pro matcher : %v\n", r) + // We are redirected to the matcher + if !strings.Contains(r, "/matcher") { + t.Errorf("no redirection to the matcher") + } + // Let's send a mail to the company CEO + do("POST", "/api/matcher/demand", noH, "angela@testcompany.com", 200, "") + fmt.Printf("Sent mail : %v\n", recorder.Msg()) + if !strings.Contains(recorder.Msg(), "Demande de mandatement") { + t.Errorf("received body is not what is expected") + } + + //////////////////////////////////////////// + // THE COMPANY CEO VALIDATES THE DEMAND // + //////////////////////////////////////////// + + // Configure FC Mock to give the ID of Angela Claire Louise DUBOIS, configure the FC Auth to callback to the matcher (what is normaly made by the js front client) + os.Setenv("FC_USER_INFO", fcServer.URL+"/userinfo") + os.Setenv("FC_AUTH", fcServer.URL+"/auth2") + franceconnect.Init() + // Create a new tester with a new cookie jar (because we are someone else, on a different computer) + _, do, _ = createTester(t) + // Extract the link from the mail + r = do("GET", "/api/matcher/validate?code="+regexp.MustCompile(`code=(.*)"`).FindStringSubmatch(recorder.Msg())[1], noH, "", 302, "") + // We are redirected to France Connect + r = redirectURIFromBody(r) + fmt.Printf("Redirected to France Connect : %v\n", r) + r = do("GET", r, noH, "", 302, "") + fmt.Printf("Body 5 : %v\n", r) + r = redirectURIFromBody(r) + r = do("GET", r, noH, "", 302, "") + fmt.Printf("Body 6 : %v\n", r) + r = redirectURIFromBody(r) + if r != "/mandatecreated" { + t.Errorf("CEO was not redirected to the mandate created information") + } + + /////////////////////////////////////////////////// + // THE MANDATEE USE THE NEWLY OBTAINED MANDATE // + /////////////////////////////////////////////////// + + // It's actually the use case 1, only with France Connect mock giving the identity of Paul Louis Dubois + os.Setenv("FC_USER_INFO", fcServer.URL+"/userinfo2") + os.Setenv("FC_AUTH", fcServer.URL+"/auth") + franceconnect.Init() + tokenMustContent = `"id_token":{"given_name":"Paul` + TestUseCase1(t) } func redirectURIFromBody(body string) string { diff --git a/pkg/middlewares/middlewares.go b/pkg/middlewares/middlewares.go index f219afc..804baad 100644 --- a/pkg/middlewares/middlewares.go +++ b/pkg/middlewares/middlewares.go @@ -34,7 +34,7 @@ type webSecurityWriter struct { } func (s webSecurityWriter) WriteHeader(code int) { - if s.wroteHeader == false { + if !s.wroteHeader { s.w.Header().Set("Strict-Transport-Security", "max-age=63072000") var inline string if s.allowEvalInlineScript { @@ -57,6 +57,7 @@ func (s webSecurityWriter) WriteHeader(code int) { s.w.Header().Set("X-XSS-Protection", "1; mode=block") s.w.Header().Set("Referrer-Policy", "strict-origin") s.w.Header().Set("X-Content-Type-Options", "nosniff") + //lint:ignore SA4005 we need to assign true so that when the WriteHeader method will be used again, we won't rewrite security headers s.wroteHeader = true } s.w.WriteHeader(code) -- GitLab