From 836df379ac958766653a28e376f4b730244cd340 Mon Sep 17 00:00:00 2001 From: reesporte Date: Fri, 21 Jan 2022 13:57:47 -0600 Subject: [PATCH] add test coverage for the following auth related packages: * authn * http * server fix minor bugs, do some cleaning up, etc in `authn/authenticate.go` and `http/handler.go` --- authn/authenticate.go | 91 ++++----- authn/authenticate_internal_test.go | 279 ++++++++++++++++++++++++++-- http/handler.go | 8 +- http/handler_internal_test.go | 187 +++++++++++++++++-- server/grpc_test.go | 42 +++++ 5 files changed, 525 insertions(+), 82 deletions(-) diff --git a/authn/authenticate.go b/authn/authenticate.go index 037d30d14..202369a90 100644 --- a/authn/authenticate.go +++ b/authn/authenticate.go @@ -117,6 +117,7 @@ type Groups struct { // Authenticate takes in a bearer token `bearer` and returns UserInfo from that token func (a *Auth) Authenticate(bearer string) (*UserInfo, error) { // parse the bearer token into a jwt.Token + // this also validates the token, and checks that it's not expired token, err := jwt.Parse(bearer, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) @@ -124,39 +125,21 @@ func (a *Auth) Authenticate(bearer string) (*UserInfo, error) { return a.secretKey, nil }) if token == nil || token.Claims == nil || err != nil || !token.Valid { - return nil, errors.Wrap(err, fmt.Sprintf("%#v parsing jwt claims from access tokens", token)) + return nil, fmt.Errorf("parsing bearer token: %v", err) } userInfo := UserInfo{} - // check that token does not expire now - switch claimType := token.Claims.(type) { - case jwt.MapClaims: - if exp, ok := claimType["exp"]; ok { - var e int64 - switch expType := exp.(type) { - case float64: - e = int64(expType) - case json.Number: - e, _ = expType.Int64() - } - if e <= time.Now().Unix() { - return nil, fmt.Errorf("token expired") - } - } - userInfo.UserID = claimType["oid"].(string) - userInfo.UserName = claimType["name"].(string) - userInfo.Token = bearer + claims := token.Claims.(jwt.MapClaims) + userInfo.UserID = claims["oid"].(string) + userInfo.UserName = claims["name"].(string) + userInfo.Token = bearer - g := claimType["molecula-idp-groups"].(string) - groups, err := FromGob64(g) - if err != nil { - return nil, errors.Wrap(err, "decoding groups") - } - userInfo.Groups = groups - - default: - return nil, fmt.Errorf("could not parse jwt claims of type %T, expected jwt.MapClaims", claimType) + g := claims["molecula-idp-groups"].(string) + groups, err := FromGob64(g) + if err != nil { + return nil, errors.Wrap(err, "decoding groups") } + userInfo.Groups = groups return &userInfo, nil } @@ -194,8 +177,16 @@ func (a *Auth) Redirect(w http.ResponseWriter, r *http.Request) { return } - // with vitamin A! - enrichedTkn, err := a.addGroupMembership(token.AccessToken) + // enrich token with groups! + g, err := a.getGroups(token.AccessToken) + if err != nil { + a.logger.Warnf("getting groups from IdP: %+v", err) + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + // with vitamin G! (for groups) + enrichedTkn, err := a.addGroupMembership(token.AccessToken, g) if err != nil { a.logger.Warnf("enriching token with group membership: %+v", err) http.Error(w, "Bad Request", http.StatusBadRequest) @@ -217,40 +208,28 @@ func (a *Auth) getToken(r *http.Request, code string) (*oauth2.Token, error) { // addGroupMembership is only called in `a.Redirect`. It adds groups to a jwt's // claims, and signs it using `a.secretKey`. -func (a *Auth) addGroupMembership(token string) (string, error) { - g, err := a.getGroups(token) - if err != nil { - return "", err - } - +func (a *Auth) addGroupMembership(token string, g []Group) (string, error) { // parse token into jwt unenriched, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{}) if unenriched == nil || unenriched.Claims == nil || err != nil { - return "", errors.Wrap(err, fmt.Sprintf("%v parsing jwt claims from access tokens", token)) + return "", fmt.Errorf("parsing bearer token: %v", err) } enriched := jwt.New(jwt.SigningMethodHS256) enriched.Claims = unenriched.Claims - var tokenStr string // parse groups into string format - switch claims := enriched.Claims.(type) { - case jwt.MapClaims: - groupString, err := ToGob64(g) - if err != nil { - return "", errors.Wrap(err, "failed to serialize groups") - } + claims := enriched.Claims.(jwt.MapClaims) + groupString, err := ToGob64(g) + if err != nil { + return "", errors.Wrap(err, "failed to serialize groups") + } + // stick it into jwt claims + claims["molecula-idp-groups"] = groupString - // stick it into jwt claims - claims["molecula-idp-groups"] = groupString - - // get stringified and signed jwt - tokenStr, err = enriched.SignedString(a.secretKey) - if err != nil { - return "", errors.Wrap(err, "signing jwt") - } - - default: - return "", fmt.Errorf("could not parse jwt claims of type %T, expected jwt.MapClaims", claims) + // get stringified and signed jwt + tokenStr, err := enriched.SignedString(a.secretKey) + if err != nil { + return "", errors.Wrap(err, "signing jwt") } return tokenStr, nil @@ -303,7 +282,7 @@ func decodeHex(hexstr string) ([]byte, error) { return nil, errors.Wrap(err, "decoding hex string to byte slice") } if len(data) != 32 { - return nil, errors.Wrap(err, "invalid key length") + return nil, fmt.Errorf("invalid key length") } return data, nil } diff --git a/authn/authenticate_internal_test.go b/authn/authenticate_internal_test.go index 3eed3878c..d4fd63c6c 100644 --- a/authn/authenticate_internal_test.go +++ b/authn/authenticate_internal_test.go @@ -1,16 +1,24 @@ package authn import ( + "bytes" + "encoding/hex" + "fmt" + "net/http" "net/http/httptest" "os" + "reflect" "strings" "testing" "time" + "github.com/golang-jwt/jwt" "github.com/molecula/featurebase/v3/logger" + "github.com/pkg/errors" ) -func TestAuth(t *testing.T) { +func NewTestAuth(t *testing.T) *Auth { + t.Helper() var ( ClientID = "e9088663-eb08-41d7-8f65-efb5f54bbb71" ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" @@ -20,7 +28,6 @@ func TestAuth(t *testing.T) { LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" - ShortKey = "DEADBEEFD" ) a, err := NewAuth( @@ -36,9 +43,13 @@ func TestAuth(t *testing.T) { Key, ) if err != nil { - t.Errorf("building auth object%s", err) + t.Fatalf("building auth object%s", err) } + return a +} +func TestAuth(t *testing.T) { + a := NewTestAuth(t) t.Run("SetCookie", func(t *testing.T) { w := httptest.NewRecorder() err := a.setCookie(w, "a cookie value", time.Now().Add(time.Hour)) @@ -53,23 +64,267 @@ func TestAuth(t *testing.T) { if got, want := w.Result().Cookies()[0].Path, "/"; got != want { t.Fatalf("path=%s, want %s", got, want) } - }) t.Run("KeyLength", func(t *testing.T) { _, err := NewAuth( logger.NewStandardLogger(os.Stdout), "http://localhost:10101/", - Scopes, - AuthorizeURL, - TokenURL, - GroupEndpointURL, - LogoutURL, - ClientID, - ClientSecret, - ShortKey, + []string{"https://graph.microsoft.com/.default", "offline_access"}, + "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize", + "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token", + "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true", + "https://login.microsoftonline.com/common/oauth2/v2.0/logout", + "e9088663-eb08-41d7-8f65-efb5f54bbb71", + "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + "DEADBEEFD", ) if err == nil || !strings.Contains(err.Error(), "decoding secret key") { t.Fatalf("expected error decoding secret key got: %v", err) } }) + t.Run("GetSecretKey", func(t *testing.T) { + want, _ := hex.DecodeString("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF") + if got := a.SecretKey(); !bytes.Equal(got, want) { + t.Fatalf("expected %v, got %v", got, want) + } + }) + cases := []struct { + name string + uid string + uname string + exp interface{} + groups []Group + err error + }{ + { + name: "GoodToken", + uid: "42", + uname: "A. Token", + groups: []Group{ + { + GroupID: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe", + GroupName: "adminGroup", + }, + }, + }, + { + name: "ExpiredToken", + uid: "42", + uname: "A. Token", + groups: []Group{ + { + GroupID: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe", + GroupName: "adminGroup", + }, + }, + exp: "-17764800", + err: errors.Wrap(fmt.Errorf("Token is expired"), "parsing bearer token"), + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + tkn := jwt.New(jwt.SigningMethodHS256) + claims := tkn.Claims.(jwt.MapClaims) + groupString, err := ToGob64(test.groups) + if err != nil { + t.Fatalf("unexpected error when gobbing groups %v", err) + } + claims["molecula-idp-groups"] = groupString + claims["oid"] = test.uid + claims["name"] = test.uname + if test.exp != nil { + claims["exp"] = test.exp + } + token, err := tkn.SignedString(a.SecretKey()) + if err != nil { + t.Fatalf("unexpected error when signing token %v", err) + } + + uinfo, err := a.Authenticate(token) + // okay this part kind of sucks bc we need to check errors and i + // dont want to write a whole new test for things that should have + // errors just to avoid this mess. errors.Is doesn't work either + if (test.err == nil && err != nil) || (test.err != nil && err == nil) { + t.Fatalf("expected %v, but got %v", test.err, err) + } else if test.err != nil && err != nil { + if test.err.Error() != err.Error() { + t.Fatalf("expected %v, but got %v", test.err, err) + } else { + return + } + } + + if !reflect.DeepEqual(uinfo.Groups, test.groups) { + t.Fatalf("expected %v, got %v", test.groups, uinfo.Groups) + } + if !reflect.DeepEqual(uinfo.UserID, test.uid) { + t.Fatalf("expected %v, got %v", test.uid, uinfo.UserID) + } + if !reflect.DeepEqual(uinfo.UserName, test.uname) { + t.Fatalf("expected %v, got %v", test.uname, uinfo.UserName) + } + }) + } +} + +func TestGobs(t *testing.T) { + t.Run("goodGob!", func(t *testing.T) { + g := []Group{ + { + GroupID: "groupA", + GroupName: "groupA-Name", + }, + { + GroupID: "groupB", + GroupName: "groupB-Name", + }, + { + GroupID: "groupC", + GroupName: "groupC-Name", + }, + } + gobbed, err := ToGob64(g) + if err != nil { + t.Fatalf("could not gob %+v", g) + } + ungobbed, err := FromGob64(gobbed) + if err != nil { + t.Fatalf("could not ungob %+v", gobbed) + } + if !reflect.DeepEqual(ungobbed, g) { + t.Fatalf("expected %v, got %v", g, ungobbed) + } + }) +} + +func TestDecodeHex(t *testing.T) { + t.Run("cantDecode", func(t *testing.T) { + _, err := decodeHex("gggg") + if err == nil { + t.Fatalf("expected err cannot decode slice, got nil") + } + }) + t.Run("tooSmall", func(t *testing.T) { + _, err := decodeHex("DEADBEEF") + if err == nil { + t.Fatalf("expected err wrong length, got nil") + } + }) + t.Run("tooBig", func(t *testing.T) { + _, err := decodeHex("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF") + if err == nil { + t.Fatalf("expected err wrong length, got nil") + } + }) + t.Run("justRight", func(t *testing.T) { + _, err := decodeHex("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF") + if err != nil { + t.Fatalf("expected nil, got %v", err) + } + }) +} + +func TestAddGroupMembership(t *testing.T) { + cases := []struct { + name string + groups []Group + err error + }{ + { + name: "emptyGroups", + groups: []Group{}, + err: nil, + }, + { + name: "happyPath", + groups: []Group{ + { + GroupID: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe", + GroupName: "adminGroup", + }, + }, + err: nil, + }, + } + a := NewTestAuth(t) + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + tkn := jwt.New(jwt.SigningMethodHS256) + token, err := tkn.SignedString(a.SecretKey()) + if err != nil { + t.Fatalf("unexpected error when signing token %v", err) + } + + tokenWithGroups, err := a.addGroupMembership(token, test.groups) + // okay this part kind of sucks bc we need to check errors and i + // dont want to write a whole new test for things that should have + // errors just to avoid this mess. errors.Is doesn't work either + if (test.err == nil && err != nil) || (test.err != nil && err == nil) { + t.Fatalf("expected %v but got %v", test.err, err) + } else if test.err != nil && err != nil { + if test.err.Error() != err.Error() { + t.Fatalf("expected %v, but got %v", test.err, err) + } else { + return + } + } + parsed, _, err := new(jwt.Parser).ParseUnverified(tokenWithGroups, jwt.MapClaims{}) + if err != nil { + t.Fatalf("unexpected error parsing token %v", err) + } + + claims := parsed.Claims.(jwt.MapClaims) + groups, err := FromGob64(claims["molecula-idp-groups"].(string)) + if err != nil { + t.Fatalf("unexpected error parsing groupString %v", err) + } + + if !reflect.DeepEqual(groups, test.groups) { + t.Fatalf("expected %v, got %v", test.groups, groups) + } + }) + } +} + +func TestHandlers(t *testing.T) { + a := NewTestAuth(t) + t.Run("login", func(t *testing.T) { + req := httptest.NewRequest("GET", "/login", nil) + w := httptest.NewRecorder() + a.Login(w, req) + resp := w.Result() + if resp.StatusCode != http.StatusTemporaryRedirect { + t.Fatalf("expected redirect, got %v", resp.StatusCode) + } + redirect := a.oAuthConfig.AuthCodeURL(a.oAuthConfig.Endpoint.AuthURL) + if got, err := resp.Location(); err != nil || got.String() != redirect { + t.Fatalf("expected %v, got %v", redirect, got.Path) + } + }) + t.Run("logout", func(t *testing.T) { + req := httptest.NewRequest("GET", "/logout", nil) + w := httptest.NewRecorder() + a.Logout(w, req) + resp := w.Result() + if resp.StatusCode != http.StatusTemporaryRedirect { + t.Fatalf("expected redirect, got %v", resp.StatusCode) + } + redirect := fmt.Sprintf("%s?post_logout_redirect_uri=%s/", a.logoutEndpoint, a.fbURL) + if got, err := resp.Location(); err != nil || got.String() != redirect { + t.Fatalf("expected %v, got %v", redirect, got.Path) + } + for _, c := range resp.Cookies() { + if c.Name == "molecula-chip" { + if c.Value != "" { + t.Fatalf("cookie not set to empty value!") + } + want := time.Unix(0, 0).Unix() + got := c.Expires.Unix() + if want != got { + t.Fatalf("expected %v, got %v", want, got) + } + break + } + } + }) } diff --git a/http/handler.go b/http/handler.go index d92f69e45..1dbf32c68 100644 --- a/http/handler.go +++ b/http/handler.go @@ -544,9 +544,13 @@ func (h *Handler) chkInternal(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if h.auth != nil { secret, ok := r.Header["X-Feature-Key"] - decodedString, err := hex.DecodeString(secret[0]) + secretString := "" + if ok { + secretString = secret[0] + } + decodedString, err := hex.DecodeString(secretString) if err != nil || !ok || !bytes.Equal(decodedString, h.auth.SecretKey()) { - http.Error(w, errors.Wrap(err, "internal secret key validation failed").Error(), http.StatusUnauthorized) + http.Error(w, "internal secret key validation failed", http.StatusUnauthorized) return } } diff --git a/http/handler_internal_test.go b/http/handler_internal_test.go index a3fcdb133..25173ede6 100644 --- a/http/handler_internal_test.go +++ b/http/handler_internal_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "io/ioutil" + "net/http" gohttp "net/http" "net/http/httptest" "net/url" @@ -235,7 +236,7 @@ func TestAuthentication(t *testing.T) { claims["name"] = "todd" validToken, err := tkn.SignedString([]byte(secretKey)) if err != nil { - panic(err) + t.Fatal(err) } validToken = "Bearer " + validToken @@ -247,15 +248,10 @@ func TestAuthentication(t *testing.T) { } // make an expired token - expiredTkn := jwt.New(jwt.SigningMethodHS256) - expiredClaims := expiredTkn.Claims.(jwt.MapClaims) - expiredClaims["molecula-idp-groups"] = groupString - expiredClaims["oid"] = "42" - expiredClaims["name"] = "todd" - expiredClaims["exp"] = "1" - expiredToken, err := expiredTkn.SignedString([]byte(secretKey)) + claims["exp"] = "1" + expiredToken, err := tkn.SignedString([]byte(secretKey)) if err != nil { - panic(err) + t.Fatal(err) } expiredToken = "Bearer " + expiredToken @@ -502,9 +498,7 @@ admin: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe"` }, }, { - // this tests that there are no permissions read in even though - // auth is turned on, so we get a 500 - name: "MW-CreateIndexGood", + name: "MW-CreateIndexInsufficientPerms", path: "/index/abcd", kind: "bearer", method: gohttp.MethodPost, @@ -622,3 +616,172 @@ admin: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe"` } } + +func TestChkAuthN(t *testing.T) { + a := NewTestAuth(t) + h := Handler{ + logger: logger.NewStandardLogger(os.Stdout), + queryLogger: logger.NewStandardLogger(os.Stdout), + auth: a, + } + + // make a valid token + tkn := jwt.New(jwt.SigningMethodHS256) + claims := tkn.Claims.(jwt.MapClaims) + groupString, _ := authn.ToGob64([]authn.Group{{GroupID: "thing", GroupName: "whatever"}}) + claims["molecula-idp-groups"] = groupString + claims["oid"] = "42" + claims["name"] = "A. Token" + validToken, err := tkn.SignedString(a.SecretKey()) + if err != nil { + t.Fatal(err) + } + validToken = "Bearer " + validToken + + // make an invalid token + invalidKey, err := hex.DecodeString("DEADBEEDDEADBEEDDEADBEEDDEADBEEDDEADBEEDDEADBEEDDEADBEEDDEADBEED") + if err != nil { + t.Fatal(err) + } + invalidToken, err := tkn.SignedString(invalidKey) + if err != nil { + t.Fatal(err) + } + invalidToken = "Bearer " + invalidToken + + // make an expired token + claims["exp"] = "1" + expiredToken, err := tkn.SignedString(a.SecretKey()) + if err != nil { + t.Fatal(err) + } + expiredToken = "Bearer " + expiredToken + + testingHandler := func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("good")) + } + + cases := []struct { + name string + endpoint string + token string + handler http.HandlerFunc + statusCode int + }{ + { + name: "Valid", + token: validToken, + handler: h.chkAuthN(testingHandler), + statusCode: http.StatusOK, + }, + { + name: "Invalid", + token: invalidToken, + handler: h.chkAuthN(testingHandler), + statusCode: http.StatusUnauthorized, + }, + { + name: "Expired", + token: expiredToken, + handler: h.chkAuthN(testingHandler), + statusCode: http.StatusUnauthorized, + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/whatever", nil) + r.Header.Add("Authorization", test.token) + test.handler(w, r) + resp := w.Result() + if resp.StatusCode != test.statusCode { + t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode) + } + }) + } +} + +func TestChkInternal(t *testing.T) { + a := NewTestAuth(t) + authKey := "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + h := Handler{ + logger: logger.NewStandardLogger(os.Stdout), + queryLogger: logger.NewStandardLogger(os.Stdout), + auth: a, + } + + testingHandler := func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("good")) + } + + cases := []struct { + name string + statusCode int + handler http.HandlerFunc + key string + }{ + { + name: "happyPath", + statusCode: http.StatusOK, + handler: h.chkInternal(testingHandler), + key: authKey, + }, + { + name: "unhappyPath-empty", + statusCode: http.StatusUnauthorized, + handler: h.chkInternal(testingHandler), + key: "", + }, + { + name: "unhappyPath-wrong", + statusCode: http.StatusUnauthorized, + handler: h.chkInternal(testingHandler), + key: "BEABBEEFBEABBEEFBEABBEEFBEABBEEFBEABBEEFBEABBEEFBEABBEEFBEABBEEF", + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/whatever", nil) + if test.key != "" { + r.Header.Add("X-Feature-Key", test.key) + } + test.handler(w, r) + resp := w.Result() + if resp.StatusCode != test.statusCode { + t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode) + } + }) + } +} + +func NewTestAuth(t *testing.T) *authn.Auth { + t.Helper() + var ( + ClientID = "e9088663-eb08-41d7-8f65-efb5f54bbb71" + ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize" + TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token" + GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true" + LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" + Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} + Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + ) + + a, err := authn.NewAuth( + logger.NewStandardLogger(os.Stdout), + "http://localhost:10101/", + Scopes, + AuthorizeURL, + TokenURL, + GroupEndpointURL, + LogoutURL, + ClientID, + ClientSecret, + Key, + ) + if err != nil { + t.Fatalf("building auth object%s", err) + } + return a +} diff --git a/server/grpc_test.go b/server/grpc_test.go index 55c9e65c1..376b44ff2 100644 --- a/server/grpc_test.go +++ b/server/grpc_test.go @@ -2,6 +2,7 @@ package server_test import ( + "bytes" "context" "encoding/hex" "fmt" @@ -1427,6 +1428,47 @@ func TestCRUDIndexes(t *testing.T) { }) } +func TestLogQuery(t *testing.T) { + method := "test!" + uinfo := authn.UserInfo{ + UserID: "ID", + UserName: "name", + } + ctx := context.WithValue(context.Background(), "userinfo", &uinfo) + + cases := []struct { + name string + req interface{} + expected string + }{ + { + name: "nonQueryReq", + req: "nope", + expected: fmt.Sprintf("GRPC: %v, %v, %v, %v, %v\n", "", []string{}, "test!", uinfo.UserID, uinfo.UserName), + }, + { + name: "QuerySQLReq", + req: &pb.QuerySQLRequest{Sql: "show fields from table"}, + expected: fmt.Sprintf("GRPC: %v, %v, %v, %v, %v, %v\n", "", []string{}, "test!", uinfo.UserID, uinfo.UserName, "show fields from table"), + }, + { + name: "QueryPQLReq", + req: &pb.QueryPQLRequest{Pql: "Count(All())"}, + expected: fmt.Sprintf("GRPC: %v, %v, %v, %v, %v, %v\n", "", []string{}, "test!", uinfo.UserID, uinfo.UserName, "Count(All())"), + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + buf := new(bytes.Buffer) + l := logger.NewStandardLogger(buf) + server.LogQuery(ctx, method, test.req, l) + if !strings.HasSuffix(buf.String(), test.expected) { + t.Errorf("expected '%v', got '%v'", test.expected, buf.String()) + } + }) + } +} + func setUpTestQuerySQLUnary(ctx context.Context, t *testing.T) (gh *server.GRPCHandler, tearDownFunc func()) { t.Helper()