diff --git a/.env.example b/.env.example index 22b25e2..77857c4 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ +export CONNECTION_STRING="mongodb+srv://USER:PASS@host.mongodb.net/?retryWrites=true&w=majority" export LAMBDA_JWT_ROUTER_HMAC_SECRET="6F697765757279746F776965757279746977756F65727974696F75776572797475696F776579727475696F776579727475696F776579727475696F7779657274" +export STAGE="development" diff --git a/Makefile b/Makefile index db03715..8984f95 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ all: format tidy build test -build: +build: tidy env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" ./... format: diff --git a/README.md b/README.md index d146c06..cfed4bd 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ Supports automatically replying to HTTP OPTIONS requests so calls from browsers Go HTTP router library for AWS API Gateway-invoked Lambda Functions Forked from [aquasecurity/lmdrouter](https://github.com/aquasecurity/lmdrouter) +## Installation +1. `go get github.com/seantcanavan/lambda_jwt_router@latest` + ## How to Build locally 1. `make build` @@ -16,8 +19,8 @@ Forked from [aquasecurity/lmdrouter](https://github.com/aquasecurity/lmdrouter) 1. `make test` ## How to Use -1. set the environment variable `LAMBDA_JWT_ROUTER_NO_CORS=true` to disable adding a CORS OPTIONS handler to every route automatically - 1. If you do not set it manually - the default value will be `*` +1. set the environment variable `LAMBDA_JWT_ROUTER_NO_CORS` to `true` to disable adding a CORS OPTIONS handler to every route automatically + 1. If you do not set it manually - the default value will be `false` (all endpoints have CORS added by default) 2. set the environment variable `LAMBDA_JWT_ROUTER_CORS_METHODS` to configure which CORS methods you would like to support 1. If you do not set it manually - the default value will be `*` 3. set the environment variable `LAMBDA_JWT_ROUTER_CORS_ORIGIN` to configure which CORS origins you would like to support @@ -26,17 +29,112 @@ Forked from [aquasecurity/lmdrouter](https://github.com/aquasecurity/lmdrouter) 1. If you do not set it manually - the default value will be `*` 5. set the environment variable `LAMBDA_JWT_ROUTER_HMAC_SECRET` to configure the HMAC secret used to encode/decode JWTs 6. See https://github.com/aquasecurity/lmdrouter for the original README and details -7. More TBD coming on how to better utilize the changes I have made -## Sample routing example +## Sample routing example - see `routing_example.go` for more detail +``` +var router *lambda_router.Router + +func init() { + router = lambda_router.NewRouter("/api") + + router.Route("DELETE", "/books/:id", books.DeleteLambda) + router.Route("GET", "/books/:id", books.GetLambda) + router.Route("POST", "/books", books.CreateLambda) + router.Route("PUT", "/books/:id", books.UpdateLambda) +} + +func main() { + // if we're running this in staging or production, we want to use the lambda handler on startup + environment := os.Getenv("STAGE") + if environment == "staging" || environment == "production" { + lambda.Start(router.Handler) + } else { // else, we want to start an HTTP server to listen for local development + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + log.Printf("Ready to listen and serve on port %s", port) + err := http.ListenAndServe(":"+port, http.HandlerFunc(router.ServeHTTP)) + if err != nil { + panic(fmt.Sprintf("http.ListAndServe error %s", err)) + } + } +} +``` + +## Sample JWT example - see `jwt_example.go` for more detail +``` +var router *lambda_router.Router + +func init() { + // implement your own base middleware functions and add to the NewRouter declaration to apply to every route + router = lambda_router.NewRouter("/api", lambda_jwt.InjectLambdaContextMW) + + // to configure middleware at the route level, add them singularly to each route + // DecodeStandard will automagically check events.Headers["Authorization"] for a valid JWT. + // It will look for the LAMBDA_JWT_ROUTER_HMAC_SECRET environment variable and use that to decode + // the JWT. If decoding succeeds, it will inject all the standard claims into the context object + // before returning so other callers can access those fields at run time. + router.Route("DELETE", "/books/:id", books.DeleteLambda, lambda_jwt.DecodeStandard) + router.Route("GET", "/books/:id", books.GetLambda, lambda_jwt.DecodeStandard) + router.Route("POST", "/books", books.CreateLambda, lambda_jwt.DecodeStandard) + router.Route("PUT", "/books/:id", books.UpdateLambda, lambda_jwt.DecodeStandard) +} -## Sample JWT example +func main() { + // if we're running this in staging or production, we want to use the lambda handler on startup + environment := os.Getenv("STAGE") + if environment == "staging" || environment == "production" { + lambda.Start(router.Handler) + } else { // else, we want to start an HTTP server to listen for local development + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + log.Printf("Ready to listen and serve on port %s", port) + err := http.ListenAndServe(":"+port, http.HandlerFunc(router.ServeHTTP)) + if err != nil { + panic(fmt.Sprintf("http.ListAndServe error %s", err)) + } + } +} +``` + +## Sample middleware example - see `middleware_example.go` for more detail +``` +var router *lambda_router.Router -## Sample middleware example +func init() { + // implement your own base middleware functions and add to the NewRouter declaration to apply to every route + router = lambda_router.NewRouter("/api", lambda_jwt.InjectLambdaContextMW) + + // to configure middleware at the route level, add them singularly to each route + router.Route("DELETE", "/books/:id", books.DeleteLambda, lambda_jwt.LogRequestMW) + router.Route("GET", "/books/:id", books.GetLambda, lambda_jwt.LogRequestMW) + router.Route("POST", "/books", books.CreateLambda, lambda_jwt.LogRequestMW) + router.Route("PUT", "/books/:id", books.UpdateLambda, lambda_jwt.LogRequestMW) +} + +func main() { + // if we're running this in staging or production, we want to use the lambda handler on startup + environment := os.Getenv("STAGE") + if environment == "staging" || environment == "production" { + lambda.Start(router.Handler) + } else { // else, we want to start an HTTP server to listen for local development + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + log.Printf("Ready to listen and serve on port %s", port) + err := http.ListenAndServe(":"+port, http.HandlerFunc(router.ServeHTTP)) + if err != nil { + panic(fmt.Sprintf("http.ListAndServe error %s", err)) + } + } +} ## All tests are passing ``` -go test -v ./... === RUN TestAllowOptionsMW === RUN TestAllowOptionsMW/verify_empty_OPTIONS_req_succeeds === RUN TestAllowOptionsMW/verify_OPTIONS_req_succeeds_with_invalid_JWT_for_AllowOptions @@ -114,13 +212,16 @@ go test -v ./... --- PASS: TestVerifyJWT/verify_err_when_parsing_invalid_jwt (0.00s) --- PASS: TestVerifyJWT/verify_err_when_parsing_expired_token_with_valid_jwt (0.00s) PASS -ok github.com/seantcanavan/lambda_jwt_router/lambda_jwt 0.005s +ok github.com/seantcanavan/lambda_jwt_router/lambda_jwt 0.004s +? github.com/seantcanavan/lambda_jwt_router/lambda_util [no test files] === RUN TestMarshalLambdaRequest === RUN TestMarshalLambdaRequest/verify_MarshalReq_correctly_adds_the_JSON_string_to_the_request_body --- PASS: TestMarshalLambdaRequest (0.00s) --- PASS: TestMarshalLambdaRequest/verify_MarshalReq_correctly_adds_the_JSON_string_to_the_request_body (0.00s) === RUN Test_UnmarshalReq === RUN Test_UnmarshalReq/valid_path&query_input +=== RUN Test_UnmarshalReq/valid_empty_input +=== RUN Test_UnmarshalReq/valid_input_unset_values === RUN Test_UnmarshalReq/invalid_path&query_input === RUN Test_UnmarshalReq/valid_body_input,_not_base64 === RUN Test_UnmarshalReq/invalid_body_input,_not_base64 @@ -128,6 +229,8 @@ ok github.com/seantcanavan/lambda_jwt_router/lambda_jwt 0.005s === RUN Test_UnmarshalReq/invalid_body_input,_base64 --- PASS: Test_UnmarshalReq (0.00s) --- PASS: Test_UnmarshalReq/valid_path&query_input (0.00s) + --- PASS: Test_UnmarshalReq/valid_empty_input (0.00s) + --- PASS: Test_UnmarshalReq/valid_input_unset_values (0.00s) --- PASS: Test_UnmarshalReq/invalid_path&query_input (0.00s) --- PASS: Test_UnmarshalReq/valid_body_input,_not_base64 (0.00s) --- PASS: Test_UnmarshalReq/invalid_body_input,_not_base64 (0.00s) @@ -247,5 +350,4 @@ ok github.com/seantcanavan/lambda_jwt_router/lambda_jwt 0.005s --- PASS: TestRouter/Overlapping_routes (0.00s) PASS ok github.com/seantcanavan/lambda_jwt_router/lambda_router 0.004s -? github.com/seantcanavan/lambda_jwt_router/lambda_util [no test files] ``` diff --git a/go.mod b/go.mod index 8dbab40..9972686 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,16 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/sync v0.4.0 // indirect + golang.org/x/text v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bd815ad..7cde79b 100644 --- a/go.sum +++ b/go.sum @@ -7,20 +7,28 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= @@ -28,6 +36,8 @@ go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwD golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -35,6 +45,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -49,6 +61,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/examples/books/books.go b/internal/examples/books/books.go new file mode 100644 index 0000000..ff5f165 --- /dev/null +++ b/internal/examples/books/books.go @@ -0,0 +1,189 @@ +package books + +import ( + "context" + "errors" + "github.com/seantcanavan/lambda_jwt_router/internal/examples/database" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Book struct { + Author string `bson:"author,omitempty" json:"author,omitempty"` + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + Pages int `bson:"pages,omitempty" json:"pages,omitempty"` + Title string `bson:"title,omitempty" json:"title,omitempty"` +} + +// Valid is a naive implementation of a validator for the Book CreateReq +// In production you should use something like https://github.com/go-playground/validator +// to perform validation in-line with unmarshalling for a more cohesive experience +func (cReq *CreateReq) Valid() error { + if cReq.Author == "" { + return errors.New("author is required") + } + + if cReq.Title == "" { + return errors.New("title is required") + } + + if cReq.Pages == 0 { + return errors.New("pages is required and must be non-zero") + } + + return nil +} + +type CreateReq struct { + Author string `json:"author,omitempty"` + Title string `json:"title,omitempty"` + Pages int `json:"pages"` +} + +func Create(ctx context.Context, cReq *CreateReq) (*Book, error) { + if err := cReq.Valid(); err != nil { + return nil, err + } + + book := &Book{ + Author: cReq.Author, + ID: primitive.NewObjectID(), + Pages: cReq.Pages, + Title: cReq.Title, + } + + _, err := database.BooksColl.InsertOne(ctx, book) + if err != nil { + return nil, err + } + + return book, nil + +} + +// Valid is a naive implementation of a validator for the Book DeleteReq +// In production you should use something like https://github.com/go-playground/validator +// to perform validation in-line with unmarshalling for a more cohesive experience +func (dReq *DeleteReq) Valid() error { + if dReq.ID.IsZero() { + return errors.New("id cannot be zero") + } + + return nil +} + +type DeleteReq struct { + ID primitive.ObjectID `lambda:"path.id" json:"id,omitempty"` +} + +func Delete(ctx context.Context, dReq *DeleteReq) (*Book, error) { + if err := dReq.Valid(); err != nil { + return nil, err + } + + singleRes := database.BooksColl.FindOneAndDelete(ctx, bson.M{"_id": dReq.ID}) + if singleRes.Err() != nil { + return nil, singleRes.Err() + } + + book := &Book{} + err := singleRes.Decode(book) + if err != nil { + return nil, err + } + + return book, nil +} + +// Valid is a naive implementation of a validator for the Book GetReq +// In production you should use something like https://github.com/go-playground/validator +// to perform validation in-line with unmarshalling for a more cohesive experience +func (gReq *GetReq) Valid() error { + if gReq.ID.IsZero() { + return errors.New("id cannot be zero") + } + + return nil +} + +type GetReq struct { + ID primitive.ObjectID `lambda:"path.id" json:"id,omitempty"` +} + +func Get(ctx context.Context, gReq *GetReq) (*Book, error) { + if err := gReq.Valid(); err != nil { + return nil, err + } + + singleRes := database.BooksColl.FindOne(ctx, bson.M{"_id": gReq.ID}) + if singleRes.Err() != nil { + return nil, singleRes.Err() + } + + book := &Book{} + err := singleRes.Decode(book) + if err != nil { + return nil, err + } + + return book, nil +} + +// Valid is a naive implementation of a validator for the Book UpdateReq +// In production you should use something like https://github.com/go-playground/validator +// to perform validation in-line with unmarshalling for a more cohesive experience +func (uReq *UpdateReq) Valid() error { + if uReq.Author == "" { + return errors.New("author is required") + } + + if uReq.ID.IsZero() { + return errors.New("id cannot be zero") + } + + if uReq.Pages == 0 { + return errors.New("pages is required and must be non-zero") + } + + if uReq.Title == "" { + return errors.New("title is required") + } + + return nil +} + +type UpdateReq struct { + Author string `json:"author,omitempty"` + ID primitive.ObjectID `lambda:"path.id" json:"id,omitempty"` + Pages int `json:"pages"` + Title string `json:"title,omitempty"` +} + +func Update(ctx context.Context, uReq *UpdateReq) (*Book, error) { + if err := uReq.Valid(); err != nil { + return nil, err + } + + singleRes := database.BooksColl.FindOneAndUpdate(ctx, bson.M{"_id": uReq.ID}, + bson.M{ + "$set": bson.M{ + "author": uReq.Author, + "title": uReq.Title, + "pages": uReq.Pages, + }, + }, + &options.FindOneAndUpdateOptions{ReturnDocument: func() *options.ReturnDocument { a := options.After; return &a }()}, + ) + if singleRes.Err() != nil { + return nil, singleRes.Err() + } + + book := &Book{} + err := singleRes.Decode(book) + if err != nil { + return nil, err + } + + return book, nil +} diff --git a/internal/examples/books/books_lambda.go b/internal/examples/books/books_lambda.go new file mode 100644 index 0000000..556b868 --- /dev/null +++ b/internal/examples/books/books_lambda.go @@ -0,0 +1,68 @@ +package books + +import ( + "context" + "github.com/aws/aws-lambda-go/events" + "github.com/seantcanavan/lambda_jwt_router/lambda_router" + "net/http" +) + +func CreateLambda(ctx context.Context, lambdaReq events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + cReq := &CreateReq{} + err := lambda_router.UnmarshalReq(lambdaReq, true, cReq) + if err != nil { + return lambda_router.StatusAndErrorRes(http.StatusInternalServerError, err) + } + + book, err := Create(ctx, cReq) + if err != nil { + return lambda_router.StatusAndErrorRes(http.StatusInternalServerError, err) + } + + return lambda_router.SuccessRes(book) +} + +func DeleteLambda(ctx context.Context, lambdaReq events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + cReq := &DeleteReq{} + err := lambda_router.UnmarshalReq(lambdaReq, false, cReq) + if err != nil { + return lambda_router.StatusAndErrorRes(http.StatusInternalServerError, err) + } + + book, err := Delete(ctx, cReq) + if err != nil { + return lambda_router.StatusAndErrorRes(http.StatusInternalServerError, err) + } + + return lambda_router.SuccessRes(book) +} + +func GetLambda(ctx context.Context, lambdaReq events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + cReq := &GetReq{} + err := lambda_router.UnmarshalReq(lambdaReq, false, cReq) + if err != nil { + return lambda_router.StatusAndErrorRes(http.StatusInternalServerError, err) + } + + book, err := Get(ctx, cReq) + if err != nil { + return lambda_router.StatusAndErrorRes(http.StatusInternalServerError, err) + } + + return lambda_router.SuccessRes(book) +} + +func UpdateLambda(ctx context.Context, lambdaReq events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + cReq := &UpdateReq{} + err := lambda_router.UnmarshalReq(lambdaReq, true, cReq) + if err != nil { + return lambda_router.StatusAndErrorRes(http.StatusInternalServerError, err) + } + + book, err := Update(ctx, cReq) + if err != nil { + return lambda_router.StatusAndErrorRes(http.StatusInternalServerError, err) + } + + return lambda_router.SuccessRes(book) +} diff --git a/internal/examples/books/books_lambda_test.go b/internal/examples/books/books_lambda_test.go new file mode 100644 index 0000000..8d1c643 --- /dev/null +++ b/internal/examples/books/books_lambda_test.go @@ -0,0 +1,84 @@ +package books + +import ( + "github.com/aws/aws-lambda-go/events" + "github.com/seantcanavan/lambda_jwt_router/internal/util" + "github.com/seantcanavan/lambda_jwt_router/lambda_router" + "github.com/stretchr/testify/require" + "testing" +) + +func TestBooksLambda(t *testing.T) { + t.Run("verify CreateLambda is working as expected", func(t *testing.T) { + cReq := &CreateReq{ + Author: util.GenerateRandomString(10), + Pages: util.GenerateRandomInt(500, 5000), + Title: util.GenerateRandomString(10), + } + + createRes, err := CreateLambda(textCtx, lambda_router.MarshalReq(cReq)) + require.NoError(t, err) + + createdBook := &Book{} + err = lambda_router.UnmarshalRes(createRes, createdBook) + require.NoError(t, err) + + require.Equal(t, cReq.Author, createdBook.Author) + require.Equal(t, cReq.Pages, createdBook.Pages) + require.Equal(t, cReq.Title, createdBook.Title) + require.False(t, createdBook.ID.IsZero()) + + t.Run("verify GetLambda is working as expected", func(t *testing.T) { + getRes, err := GetLambda(textCtx, events.APIGatewayProxyRequest{PathParameters: map[string]string{"id": createdBook.ID.Hex()}}) + require.NoError(t, err) + + gotBook := &Book{} + + err = lambda_router.UnmarshalRes(getRes, gotBook) + require.NoError(t, err) + + require.Equal(t, createdBook.Title, gotBook.Title) + require.Equal(t, createdBook.Author, gotBook.Author) + require.Equal(t, createdBook.ID, gotBook.ID) + require.Equal(t, createdBook.Pages, gotBook.Pages) + }) + + t.Run("verify UpdateLambda is working as expected", func(t *testing.T) { + uReq := &UpdateReq{ + Author: util.GenerateRandomString(10), + Pages: util.GenerateRandomInt(500, 5000), + Title: util.GenerateRandomString(10), + } + + lambdaReq := lambda_router.MarshalReq(uReq) + lambdaReq.PathParameters = map[string]string{"id": createdBook.ID.Hex()} + + updateRes, err := UpdateLambda(textCtx, lambdaReq) + require.NoError(t, err) + + updatedBook := &Book{} + err = lambda_router.UnmarshalRes(updateRes, updatedBook) + require.NoError(t, err) + + require.Equal(t, uReq.Author, updatedBook.Author) + require.Equal(t, uReq.Title, updatedBook.Title) + require.Equal(t, uReq.Pages, updatedBook.Pages) + require.Equal(t, createdBook.ID, updatedBook.ID) + + t.Run("verify DeleteLambda is working as expected", func(t *testing.T) { + deleteRes, err := DeleteLambda(textCtx, events.APIGatewayProxyRequest{PathParameters: map[string]string{"id": createdBook.ID.Hex()}}) + require.NoError(t, err) + + deletedBook := &Book{} + err = lambda_router.UnmarshalRes(deleteRes, deletedBook) + require.NoError(t, err) + + require.Equal(t, updatedBook.Title, deletedBook.Title) + require.Equal(t, updatedBook.Author, deletedBook.Author) + require.Equal(t, updatedBook.ID, deletedBook.ID) + require.Equal(t, updatedBook.Pages, deletedBook.Pages) + }) + }) + }) + +} diff --git a/internal/examples/books/books_test.go b/internal/examples/books/books_test.go new file mode 100644 index 0000000..4fcbb12 --- /dev/null +++ b/internal/examples/books/books_test.go @@ -0,0 +1,93 @@ +package books + +import ( + "context" + "fmt" + "github.com/joho/godotenv" + "github.com/seantcanavan/lambda_jwt_router/internal/examples/database" + "github.com/seantcanavan/lambda_jwt_router/internal/util" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +var textCtx = context.Background() + +func TestMain(m *testing.M) { + setup() + m.Run() + tearDown() +} + +func setup() { + err := godotenv.Load("../../../.env") + if err != nil { + panic(fmt.Sprintf("Unable to load .env file: %s", err)) + } + + time.Local = time.UTC + database.Connect() +} + +func tearDown() { + database.Disconnect() +} + +func TestBooks(t *testing.T) { + t.Run("verify Create is working as expected", func(t *testing.T) { + cReq := &CreateReq{ + Author: util.GenerateRandomString(10), + Pages: util.GenerateRandomInt(500, 5000), + Title: util.GenerateRandomString(10), + } + + createdBook, err := Create(textCtx, cReq) + require.NoError(t, err) + + require.Equal(t, cReq.Author, createdBook.Author) + require.Equal(t, cReq.Pages, createdBook.Pages) + require.Equal(t, cReq.Title, createdBook.Title) + require.False(t, createdBook.ID.IsZero()) + + t.Run("verify Get is working as expected", func(t *testing.T) { + gReq := &GetReq{ID: createdBook.ID} + + book, err := Get(textCtx, gReq) + require.NoError(t, err) + + require.Equal(t, createdBook.Title, book.Title) + require.Equal(t, createdBook.Author, book.Author) + require.Equal(t, createdBook.ID, book.ID) + require.Equal(t, createdBook.Pages, book.Pages) + }) + + t.Run("verify Update is working as expected", func(t *testing.T) { + uReq := &UpdateReq{ + Author: util.GenerateRandomString(10), + ID: createdBook.ID, + Pages: util.GenerateRandomInt(500, 5000), + Title: util.GenerateRandomString(10), + } + + updatedBook, err := Update(textCtx, uReq) + require.NoError(t, err) + + require.Equal(t, uReq.Author, updatedBook.Author) + require.Equal(t, uReq.Title, updatedBook.Title) + require.Equal(t, uReq.Pages, updatedBook.Pages) + require.Equal(t, uReq.ID, createdBook.ID) + + t.Run("verify Delete is working as expected", func(t *testing.T) { + dReq := &DeleteReq{ID: createdBook.ID} + + deletedBook, err := Delete(textCtx, dReq) + require.NoError(t, err) + + require.Equal(t, updatedBook.Title, deletedBook.Title) + require.Equal(t, updatedBook.Author, deletedBook.Author) + require.Equal(t, updatedBook.ID, deletedBook.ID) + require.Equal(t, updatedBook.Pages, deletedBook.Pages) + }) + }) + }) +} diff --git a/internal/examples/database/database.go b/internal/examples/database/database.go new file mode 100644 index 0000000..721382d --- /dev/null +++ b/internal/examples/database/database.go @@ -0,0 +1,66 @@ +package database + +import ( + "context" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "os" + "time" +) + +var Client *mongo.Client +var Database *mongo.Database + +var BooksColl *mongo.Collection + +func Connect() { + name := "lambda_jwt_router" + connectionString := os.Getenv("CONNECTION_STRING") + ctx := context.Background() + + var err error + Client, err = mongo.Connect(ctx, options.Client().ApplyURI(connectionString)) + if err != nil { + panic(fmt.Sprintf("mongo.NewClient failed with error %s", err)) + } + + Database = Client.Database(name) + + BooksColl = Database.Collection("books") + + createIndexesOptions := options.CreateIndexesOptions{ + CommitQuorum: "majority", + } + + _, err = BooksColl.Indexes().CreateMany(ctx, getBookIndexes(), &createIndexesOptions) + if err != nil { + panic(fmt.Sprintf("BooksColl.Indexes().CreateMany failed with error %s", err)) + } +} + +func getBookIndexes() []mongo.IndexModel { + var indexes []mongo.IndexModel + + // create an index on fullName, so we can search by name + authorTitleUniqueIndex := mongo.IndexModel{ + Keys: bson.D{{"author", 1}, {"title", 1}}, + Options: &options.IndexOptions{ + Unique: func() *bool { a := true; return &a }(), + }, + } + indexes = append(indexes, authorTitleUniqueIndex) + + return indexes +} + +func Disconnect() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + err := Client.Disconnect(ctx) + if err != nil { + panic(fmt.Sprintf("Client.Disconnect(ctx) failed with error %s", err)) + } + + defer cancel() +} diff --git a/internal/examples/jwt/jwt_example.go b/internal/examples/jwt/jwt_example.go new file mode 100644 index 0000000..6e5aec6 --- /dev/null +++ b/internal/examples/jwt/jwt_example.go @@ -0,0 +1,47 @@ +package jwt + +import ( + "fmt" + "github.com/aws/aws-lambda-go/lambda" + "github.com/seantcanavan/lambda_jwt_router/internal/examples/books" + "github.com/seantcanavan/lambda_jwt_router/lambda_jwt" + "github.com/seantcanavan/lambda_jwt_router/lambda_router" + "log" + "net/http" + "os" +) + +var router *lambda_router.Router + +func init() { + // implement your own base middleware functions and add to the NewRouter declaration to apply to every route + router = lambda_router.NewRouter("/api", lambda_jwt.InjectLambdaContextMW) + + // to configure middleware at the route level, add them singularly to each route + // DecodeStandard will automagically check events.Headers["Authorization"] for a valid JWT. + // It will look for the LAMBDA_JWT_ROUTER_HMAC_SECRET environment variable and use that to decode + // the JWT. If decoding succeeds, it will inject all the standard claims into the context object + // before returning so other callers can access those fields at run time. + router.Route("DELETE", "/books/:id", books.DeleteLambda, lambda_jwt.DecodeStandard) + router.Route("GET", "/books/:id", books.GetLambda, lambda_jwt.DecodeStandard) + router.Route("POST", "/books", books.CreateLambda, lambda_jwt.DecodeStandard) + router.Route("PUT", "/books/:id", books.UpdateLambda, lambda_jwt.DecodeStandard) +} + +func main() { + // if we're running this in staging or production, we want to use the lambda handler on startup + environment := os.Getenv("STAGE") + if environment == "staging" || environment == "production" { + lambda.Start(router.Handler) + } else { // else, we want to start an HTTP server to listen for local development + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + log.Printf("Ready to listen and serve on port %s", port) + err := http.ListenAndServe(":"+port, http.HandlerFunc(router.ServeHTTP)) + if err != nil { + panic(fmt.Sprintf("http.ListAndServe error %s", err)) + } + } +} diff --git a/internal/examples/middleware/middleware_example.go b/internal/examples/middleware/middleware_example.go new file mode 100644 index 0000000..6157b2d --- /dev/null +++ b/internal/examples/middleware/middleware_example.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "fmt" + "github.com/aws/aws-lambda-go/lambda" + "github.com/seantcanavan/lambda_jwt_router/internal/examples/books" + "github.com/seantcanavan/lambda_jwt_router/lambda_jwt" + "github.com/seantcanavan/lambda_jwt_router/lambda_router" + "log" + "net/http" + "os" +) + +var router *lambda_router.Router + +func init() { + // implement your own base middleware functions and add to the NewRouter declaration to apply to every route + router = lambda_router.NewRouter("/api", lambda_jwt.InjectLambdaContextMW) + + // to configure middleware at the route level, add them singularly to each route + router.Route("DELETE", "/books/:id", books.DeleteLambda, lambda_jwt.LogRequestMW) + router.Route("GET", "/books/:id", books.GetLambda, lambda_jwt.LogRequestMW) + router.Route("POST", "/books", books.CreateLambda, lambda_jwt.LogRequestMW) + router.Route("PUT", "/books/:id", books.UpdateLambda, lambda_jwt.LogRequestMW) +} + +func main() { + // if we're running this in staging or production, we want to use the lambda handler on startup + environment := os.Getenv("STAGE") + if environment == "staging" || environment == "production" { + lambda.Start(router.Handler) + } else { // else, we want to start an HTTP server to listen for local development + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + log.Printf("Ready to listen and serve on port %s", port) + err := http.ListenAndServe(":"+port, http.HandlerFunc(router.ServeHTTP)) + if err != nil { + panic(fmt.Sprintf("http.ListAndServe error %s", err)) + } + } +} diff --git a/internal/examples/routing/routing_example.go b/internal/examples/routing/routing_example.go new file mode 100644 index 0000000..ca0635e --- /dev/null +++ b/internal/examples/routing/routing_example.go @@ -0,0 +1,40 @@ +package routing + +import ( + "fmt" + "github.com/aws/aws-lambda-go/lambda" + "github.com/seantcanavan/lambda_jwt_router/internal/examples/books" + "github.com/seantcanavan/lambda_jwt_router/lambda_router" + "log" + "net/http" + "os" +) + +var router *lambda_router.Router + +func init() { + router = lambda_router.NewRouter("/api") + + router.Route("DELETE", "/books/:id", books.DeleteLambda) + router.Route("GET", "/books/:id", books.GetLambda) + router.Route("POST", "/books", books.CreateLambda) + router.Route("PUT", "/books/:id", books.UpdateLambda) +} + +func main() { + // if we're running this in staging or production, we want to use the lambda handler on startup + environment := os.Getenv("STAGE") + if environment == "staging" || environment == "production" { + lambda.Start(router.Handler) + } else { // else, we want to start an HTTP server to listen for local development + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + log.Printf("Ready to listen and serve on port %s", port) + err := http.ListenAndServe(":"+port, http.HandlerFunc(router.ServeHTTP)) + if err != nil { + panic(fmt.Sprintf("http.ListAndServe error %s", err)) + } + } +} diff --git a/lambda_util/util.go b/internal/util/util.go similarity index 95% rename from lambda_util/util.go rename to internal/util/util.go index 593f5ce..b2f2e26 100644 --- a/lambda_util/util.go +++ b/internal/util/util.go @@ -1,4 +1,4 @@ -package lambda_util +package util import ( "encoding/json" @@ -69,6 +69,11 @@ func GenerateRandomAPIGatewayContext() events.APIGatewayProxyRequestContext { } } +// GenerateRandomInt returns a random integer between N and M (inclusive). +func GenerateRandomInt(N, M int) int { + return rand.Intn(M-N+1) + N +} + func GenerateRandomString(n int) string { var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") rand.Seed(time.Now().UnixNano()) diff --git a/lambda_jwt/jwt_auth_lambda.go b/lambda_jwt/jwt_auth_lambda.go index d029a1e..a52740d 100644 --- a/lambda_jwt/jwt_auth_lambda.go +++ b/lambda_jwt/jwt_auth_lambda.go @@ -11,6 +11,13 @@ import ( "strings" ) +const MethodKey = "method" +const MultiParamsKey = "multiParams" +const PathKey = "path" +const PathParamsKey = "pathParams" +const QueryParamsKey = "queryParams" +const RequestIDKey = "requestId" + var ErrNoAuthorizationHeader = errors.New("no Authorization header value set") var ErrNoBearerPrefix = errors.New("missing 'Bearer ' prefix for Authorization header value") var ErrVerifyJWT = errors.New("unable to verify JWT to retrieve claims. try logging in again to ensure it is not expired") @@ -173,3 +180,21 @@ func LogRequestMW(next lambda_router.Handler) lambda_router.Handler { return res, err } } + +// InjectLambdaContextMW with do exactly that - inject all appropriate lambda values into the local +// context so that other users down the line can query the context for things like HTTP method or Path +func InjectLambdaContextMW(next lambda_router.Handler) lambda_router.Handler { + return func(ctx context.Context, req events.APIGatewayProxyRequest) ( + res events.APIGatewayProxyResponse, + err error, + ) { + ctx = context.WithValue(ctx, MethodKey, req.HTTPMethod) + ctx = context.WithValue(ctx, MultiParamsKey, req.MultiValueQueryStringParameters) + ctx = context.WithValue(ctx, PathKey, req.Path) + ctx = context.WithValue(ctx, PathParamsKey, req.PathParameters) + ctx = context.WithValue(ctx, QueryParamsKey, req.QueryStringParameters) + ctx = context.WithValue(ctx, RequestIDKey, req.RequestContext.RequestID) + + return next(ctx, req) + } +} diff --git a/lambda_jwt/jwt_auth_lambda_test.go b/lambda_jwt/jwt_auth_lambda_test.go index 10cd997..7ba280b 100644 --- a/lambda_jwt/jwt_auth_lambda_test.go +++ b/lambda_jwt/jwt_auth_lambda_test.go @@ -5,8 +5,8 @@ import ( "errors" "github.com/aws/aws-lambda-go/events" "github.com/golang-jwt/jwt" + "github.com/seantcanavan/lambda_jwt_router/internal/util" "github.com/seantcanavan/lambda_jwt_router/lambda_router" - "github.com/seantcanavan/lambda_jwt_router/lambda_util" "github.com/stretchr/testify/require" "net/http" "testing" @@ -18,7 +18,7 @@ func TestAllowOptionsMW(t *testing.T) { req := events.APIGatewayProxyRequest{ HTTPMethod: http.MethodOptions, Headers: nil, - RequestContext: lambda_util.GenerateRandomAPIGatewayContext(), + RequestContext: util.GenerateRandomAPIGatewayContext(), } // we pass along an error handler but expect http.StatusOK because the AllowOptions handler should execute first @@ -41,7 +41,7 @@ func TestAllowOptionsMW(t *testing.T) { Headers: map[string]string{ "Authorization": "Bearer " + signedJWT, }, - RequestContext: lambda_util.GenerateRandomAPIGatewayContext(), + RequestContext: util.GenerateRandomAPIGatewayContext(), } // we pass along an error handler but expect http.StatusOK because the AllowOptions handler should execute first @@ -57,7 +57,7 @@ func TestAllowOptionsMW(t *testing.T) { req := events.APIGatewayProxyRequest{ HTTPMethod: http.MethodOptions, Headers: nil, - RequestContext: lambda_util.GenerateRandomAPIGatewayContext(), + RequestContext: util.GenerateRandomAPIGatewayContext(), } // we pass along an error handler but expect http.StatusOK because the AllowOptions handler should execute first @@ -97,7 +97,7 @@ func TestDecodeAndInjectExpandedClaims(t *testing.T) { Headers: map[string]string{ "Authorization": "Bearer " + signedJWT, }, - RequestContext: lambda_util.GenerateRandomAPIGatewayContext(), + RequestContext: util.GenerateRandomAPIGatewayContext(), } jwtMiddlewareHandler := DecodeExpanded(generateSuccessHandlerAndMapExpandedContext()) @@ -157,7 +157,7 @@ func TestDecodeAndInjectStandardClaims(t *testing.T) { Headers: map[string]string{ "Authorization": "Bearer " + signedJWT, }, - RequestContext: lambda_util.GenerateRandomAPIGatewayContext(), + RequestContext: util.GenerateRandomAPIGatewayContext(), } jwtMiddlewareHandler := DecodeStandard(generateSuccessHandlerAndMapStandardContext()) @@ -265,7 +265,7 @@ func TestExtractJWT(t *testing.T) { func TestGenerateEmptyErrorHandler(t *testing.T) { t.Run("verify empty error handler returns error", func(t *testing.T) { errHandler := GenerateEmptyErrorHandler() - res, err := errHandler(nil, lambda_util.GenerateRandomAPIGatewayProxyRequest()) + res, err := errHandler(nil, util.GenerateRandomAPIGatewayProxyRequest()) require.Nil(t, err) // err handler embeds the error in the response, not the golang stack require.Equal(t, res.StatusCode, http.StatusInternalServerError) var httpError lambda_router.HTTPError @@ -279,7 +279,7 @@ func TestGenerateEmptyErrorHandler(t *testing.T) { func TestGenerateEmptySuccessHandler(t *testing.T) { t.Run("verify empty success handler returns success", func(t *testing.T) { successHandler := GenerateEmptySuccessHandler() - res, err := successHandler(nil, lambda_util.GenerateRandomAPIGatewayProxyRequest()) + res, err := successHandler(nil, util.GenerateRandomAPIGatewayProxyRequest()) require.Nil(t, err) require.Equal(t, res.StatusCode, http.StatusOK) require.Equal(t, res.Body, "{}") // empty struct response diff --git a/lambda_jwt/lambda_jwt_test.go b/lambda_jwt/lambda_jwt_test.go index 9632ae5..a194bc1 100644 --- a/lambda_jwt/lambda_jwt_test.go +++ b/lambda_jwt/lambda_jwt_test.go @@ -4,7 +4,7 @@ import ( "errors" "github.com/golang-jwt/jwt" "github.com/joho/godotenv" - "github.com/seantcanavan/lambda_jwt_router/lambda_util" + "github.com/seantcanavan/lambda_jwt_router/internal/util" "github.com/stretchr/testify/require" "log" "strings" @@ -26,18 +26,18 @@ func setup() { func TestExtendExpandedClaims(t *testing.T) { expandedClaims := ExpandedClaims{ - Audience: lambda_util.GenerateRandomString(10), - Email: lambda_util.GenerateRandomString(10), + Audience: util.GenerateRandomString(10), + Email: util.GenerateRandomString(10), ExpiresAt: time.Now().Add(time.Hour * 30).Unix(), - FirstName: lambda_util.GenerateRandomString(10), - FullName: lambda_util.GenerateRandomString(10), - ID: lambda_util.GenerateRandomString(10), + FirstName: util.GenerateRandomString(10), + FullName: util.GenerateRandomString(10), + ID: util.GenerateRandomString(10), IssuedAt: time.Now().Unix(), - Issuer: lambda_util.GenerateRandomString(10), - Level: lambda_util.GenerateRandomString(10), + Issuer: util.GenerateRandomString(10), + Level: util.GenerateRandomString(10), NotBefore: time.Now().Add(time.Hour * -1).Unix(), - Subject: lambda_util.GenerateRandomString(10), - UserType: lambda_util.GenerateRandomString(10), + Subject: util.GenerateRandomString(10), + UserType: util.GenerateRandomString(10), } extendedClaims := ExtendExpanded(expandedClaims) @@ -74,13 +74,13 @@ func TestExtendExpandedClaims(t *testing.T) { func TestExtendStandardClaims(t *testing.T) { standardClaims := jwt.StandardClaims{ - Audience: lambda_util.GenerateRandomString(10), + Audience: util.GenerateRandomString(10), ExpiresAt: time.Now().Add(time.Hour * 30).Unix(), - Id: lambda_util.GenerateRandomString(10), + Id: util.GenerateRandomString(10), IssuedAt: time.Now().Unix(), - Issuer: lambda_util.GenerateRandomString(10), + Issuer: util.GenerateRandomString(10), NotBefore: time.Now().Add(time.Hour * -1).Unix(), - Subject: lambda_util.GenerateRandomString(10), + Subject: util.GenerateRandomString(10), } extendedClaims := ExtendStandard(standardClaims) @@ -118,7 +118,7 @@ func TestExtractCustomClaims(t *testing.T) { ExpiresAt int64 `json:"exp"` } extractCustomErr := ExtractCustom(jwt.MapClaims{ - "exp": lambda_util.GenerateRandomString(10), // exp should be an integer + "exp": util.GenerateRandomString(10), // exp should be an integer }, &badClaims{}) require.NotNil(t, extractCustomErr) @@ -149,7 +149,7 @@ func TestExtractCustomClaims(t *testing.T) { func TestExtractStandardClaims(t *testing.T) { t.Run("verify ExtractStandard returns an err when unmarshalling to invalid standard claims object", func(t *testing.T) { extractCustomErr := ExtractStandard(jwt.MapClaims{ - "exp": lambda_util.GenerateRandomString(10), // exp should be an integer + "exp": util.GenerateRandomString(10), // exp should be an integer }, &jwt.StandardClaims{}) require.NotNil(t, extractCustomErr) @@ -183,7 +183,7 @@ func TestSign(t *testing.T) { func TestVerifyJWT(t *testing.T) { t.Run("verify err when parsing invalid jwt", func(t *testing.T) { - _, err := VerifyJWT(lambda_util.GenerateRandomString(10)) + _, err := VerifyJWT(util.GenerateRandomString(10)) require.NotNil(t, err) require.True(t, errors.Is(err, ErrInvalidJWT)) }) @@ -202,29 +202,29 @@ func TestVerifyJWT(t *testing.T) { func generateExpandedMapClaims() jwt.MapClaims { return jwt.MapClaims{ - AudienceKey: lambda_util.GenerateRandomString(10), - EmailKey: lambda_util.GenerateRandomString(10), + AudienceKey: util.GenerateRandomString(10), + EmailKey: util.GenerateRandomString(10), ExpiresAtKey: time.Now().Add(time.Hour * 30).Unix(), - FirstNameKey: lambda_util.GenerateRandomString(10), - FullNameKey: lambda_util.GenerateRandomString(10), - IDKey: lambda_util.GenerateRandomString(10), + FirstNameKey: util.GenerateRandomString(10), + FullNameKey: util.GenerateRandomString(10), + IDKey: util.GenerateRandomString(10), IssuedAtKey: time.Now().Unix(), - IssuerKey: lambda_util.GenerateRandomString(10), - LevelKey: lambda_util.GenerateRandomString(10), + IssuerKey: util.GenerateRandomString(10), + LevelKey: util.GenerateRandomString(10), NotBeforeKey: time.Now().Add(time.Hour * -1).Unix(), - SubjectKey: lambda_util.GenerateRandomString(10), - UserTypeKey: lambda_util.GenerateRandomString(10), + SubjectKey: util.GenerateRandomString(10), + UserTypeKey: util.GenerateRandomString(10), } } func generateStandardMapClaims() jwt.MapClaims { return jwt.MapClaims{ - AudienceKey: lambda_util.GenerateRandomString(10), + AudienceKey: util.GenerateRandomString(10), ExpiresAtKey: time.Now().Add(time.Hour * 30).Unix(), - IDKey: lambda_util.GenerateRandomString(10), + IDKey: util.GenerateRandomString(10), IssuedAtKey: time.Now().Unix(), - IssuerKey: lambda_util.GenerateRandomString(10), + IssuerKey: util.GenerateRandomString(10), NotBeforeKey: time.Now().Add(time.Hour * -1).Unix(), - SubjectKey: lambda_util.GenerateRandomString(10), + SubjectKey: util.GenerateRandomString(10), } } diff --git a/lambda_router/decoder_test.go b/lambda_router/decoder_test.go index b5a7e99..e861eb3 100644 --- a/lambda_router/decoder_test.go +++ b/lambda_router/decoder_test.go @@ -2,7 +2,7 @@ package lambda_router import ( "cloud.google.com/go/civil" - "github.com/seantcanavan/lambda_jwt_router/lambda_util" + "github.com/seantcanavan/lambda_jwt_router/internal/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson/primitive" @@ -87,8 +87,8 @@ const aliasExample stringAliasExample = "world" func TestMarshalLambdaRequest(t *testing.T) { mi := mockItem{ - ID: lambda_util.GenerateRandomString(10), - Name: lambda_util.GenerateRandomString(10), + ID: util.GenerateRandomString(10), + Name: util.GenerateRandomString(10), } t.Run("verify MarshalReq correctly adds the JSON string to the request body", func(t *testing.T) {