Related
I write this go code for login. Now i want to unit test my code. This code is depends on controller to service layer then service to repository layer. I want to use gomock tool for mocking, if any other please suggest me. I'm using echo framework.
Here
serializers.LoginReq =
{
Email string,
Phone string,
Admin bool
}
type auth struct {
authSvc svc.IAuth
userSvc svc.IUsers
}
func NewAuthController(grp interface {}, authSvc svc.IAuth, userSvc svc.IUsers) {
ac: = & auth {
authSvc: authSvc,
userSvc: userSvc,
}
g: = grp.( * echo.Group)
g.POST("/v1/login", ac.Login)
}
func(ctr * auth) Login(c echo.Context) error {
var cred * serializers.LoginReq
var resp * serializers.LoginResp
var err error
if err = c.Bind( & cred) err != nil {
return c.JSON(err.Status, err)
}
if resp, err = ctr.authSvc.Login(cred); err != nil {
return c.JSON(err.Status, err)
}
return c.JSON(http.StatusOK, resp)
}
Use dependency injection. Dependency injection is a design pattern that decouples dependencies between two or more layers of software.
How it works
Pass a dependency to the Login function. In Go, the dependency is often an interface type. Interfaces express generalizations or abstractions about the behaviors of other types. A type satisfies an interface if it has all the methods in the interface. With an interface, you can replace a real object with a fake one (a mock) in your tests. This works without Go's type system complaining as long as a concrete type satisfies the interface.
type Auther interface {
Login(cred *serializers.LoginReq) (*serializers.LoginResp, error)
}
Go Interfaces are satisfied implicitly.
// auth service must implement the Auther interface
type auth struct {
authSvc Auther
}
// route handler
func(ctr *auth) Login(c echo.Context) error {
var cred * serializers.LoginReq
var resp * serializers.LoginResp
var err error
if err = c.Bind( & cred) err != nil {
return c.JSON(err.Status, err)
}
// the function signature of the service-level Login method must match the interface
if resp, err = ctr.authSvc.Login(cred); err != nil {
return c.JSON(err.Status, err)
}
return c.JSON(http.StatusOK, resp)
}
I like using testify/mock library. Create a Mock.
type MockAuth struct {
mock.Mock
}
func (m *MockAuth) Login(cred *serializers.LoginReq) (*serializers.LoginResp, error) {
args := m.Called(cred)
return args.Get(0).(*serializers.LoginResp), args.Error(1)
}
That's it. Just create a test.
func TestLogin (t *testing.T) {
// setup mocks
cred := &serializers.LoginReq{}
mockReturn := &serializers.LoginResp{}
mockAuth := &MockAuth{}
// setup expectation
mockAuth.On("Login", cred).Return(mockReturn, nil)
// setup server
mux := http.NewServeMux()
mux.HandleFunc("/v1/login", func(w http.ResponseWriter, r *http.Request) {
ec := echo.Context{}
ctr: = &auth {
authSvc: mockAuth
}
ctr.Login(ec)
})
// make request
writer := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodPost, "/v1/login", "password")
mux.ServeHTTP(writer, request)
// make assertions
mockAuth.AssertExpectations(t)
}
The code above is not 100% correct. I don't use echo myself, however it should get you close. Hope this helps.
How can I validate and get info from a JWT received from Amazon Cognito?
I have setup Google authentication in Cognito, and set the redirect uri to to hit API Gateway, I then receive a code which I POST to this endpoint:
https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html
To receive the JWT token, in a RS256 format. I am now struggling to validate, and parse the token in Golang. I’ve tried to parse it using jwt-go, but it appears to support HMAC instead by default and read somewhere that they recommend using frontend validation instead. I tried a few other packages and had similar problems.
I came across this answer here: Go Language and Verify JWT but assume the code is outdated as that just says panic: unable to find key.
jwt.io can easily decode the key, and probably verify too. I’m not sure where the public/secret keys are as Amazon generated the token, but from what I understand I need to use a JWK URL to validate too? I’ve found a few AWS specific solutions, but they all seem to be hundreds of lines long. Surely it isn’t that complicated in Golang is it?
Public keys for Amazon Cognito
As you already guessed, you'll need the public key in order to verify the JWT token.
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-step-2
Download and store the corresponding public JSON Web Key (JWK) for your user pool. It is available as part of a JSON Web Key Set (JWKS).
You can locate it at
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
Parse keys and verify token
That JSON file structure is documented in the web, so you could potentially parse that manually, generate the public keys, etc.
But it'd probably be easier to just use a library, for example this one:
https://github.com/lestrrat-go/jwx
And then jwt-go to deal with the JWT part: https://github.com/dgrijalva/jwt-go
You can then:
Download and parse the public keys JSON using the first library
keySet, err := jwk.Fetch(THE_COGNITO_URL_DESCRIBED_ABOVE)
When parsing the token with jwt-go, use the "kid" field from the JWT header to find the right key to use
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRS256); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
keys := keySet.LookupKeyID(kid);
if !ok {
return nil, fmt.Errorf("key with specified kid is not present in jwks")
}
var publickey interface{}
err = keys.Raw(&publickey)
if err != nil {
return nil, fmt.Errorf("could not parse pubkey")
}
return publickey, nil
The type assertion in the code provided by eugenioy and Kevin Wydler did not work for me: *jwt.SigningMethodRS256 is not a type.
*jwt.SigningMethodRS256 was a type in the initial commit. From the second commit on (back in July 2014) it was abstracted and replaced by a global variable (see here).
This following code works for me:
func verify(tokenString string, keySet *jwk.Set) {
tkn, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != "RSA256" { // jwa.RS256.String() works as well
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
keys := keySet.LookupKeyID(kid)
if len(keys) == 0 {
return nil, fmt.Errorf("key %v not found", kid)
}
var raw interface{}
return raw, keys[0].Raw(&raw)
})
}
Using the following dependency versions:
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/lestrrat-go/jwx v1.0.4
This is what I did with only the latest (v1.0.8) github.com/lestrrat-go/jwx. Note that github.com/dgrijalva/jwt-go does not seem to be maintained anymore and people are forking it to make the updates they need.
package main
import (
...
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
)
...
keyset, err := jwk.Fetch("https://cognito-idp." + region + ".amazonaws.com/" + userPoolID + "/.well-known/jwks.json")
parsedToken, err := jwt.Parse(
bytes.NewReader(token), //token is a []byte
jwt.WithKeySet(keyset),
jwt.WithValidate(true),
jwt.WithIssuer(...),
jwt.WithClaimValue("key", value),
)
//check err as usual
//here you can call methods on the parsedToken to get the claim values
...
Token claim methods
eugenioy's answer stopped working for me because of this refactor. I ended up fixing with something like this
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRS256); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
keys := keySet.LookupKeyID(kid);
if len(keys) == 0 {
return nil, fmt.Errorf("key %v not found", kid)
}
// keys[0].Materialize() doesn't exist anymore
var raw interface{}
return raw, keys[0].Raw(&raw)
})
A newer method to achieve verification and access the token is to use Gin Cognito JWT Authentication Middleware:
package main
import (
jwtCognito "github.com/akhettar/gin-jwt-cognito"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"log"
)
func main() {
r := gin.Default()
// Create the authentication middleware
mw, err := jwtCognito.AuthJWTMiddleware(<iss>, <user_pool_id>, <region>)
if err != nil {
panic(err)
}
r.GET("/someGet", mw.MiddlewareFunc(), func(c *gin.Context) {
// Get the token
tokenStr, _ := c.Get("JWT_TOKEN")
token := tokenStr.(*jwt.Token)
// Cast the claims
claims := token.Claims.(jwt.MapClaims)
log.Printf("userCognitoId=%v", claims["cognito:username"])
log.Printf("userName=%v", claims["name"])
c.Status(http.StatusOK)
})
// By default it serves on :8080
r.Run()
}
This is what worked for me:
import (
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/jwk"
"net/http"
"os"
)
func verifyToken(token *jwt.Token) (interface{}, error) {
// make sure to replace this with your actual URL
// https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-step-2
jwksURL := "COGNITO_JWKS_URL"
set, err := jwk.FetchHTTP(jwksURL)
if err != nil {
return nil, err
}
keyID, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("expecting JWT header to have string kid")
}
keys := set.LookupKeyID(keyID)
if len(keys) == 0 {
return nil, fmt.Errorf("key %v not found", keyID)
}
if key := set.LookupKeyID(keyID); len(key) == 1 {
return key[0].Materialize()
}
return nil, fmt.Errorf("unable to find key %q", keyID)
}
I am calling it like this (using AWS Lambda gin) in my case. If you are using a different way of managing requests, make sure to replace that with http.Request or any other framework that you might be using:
func JWTVerify() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("AccessToken")
_, err := jwt.Parse(tokenString, verifyToken)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
}
}
}
This is my go.mod:
module MY_MODULE_NAME
go 1.12
require (
github.com/aws/aws-lambda-go v1.20.0
github.com/aws/aws-sdk-go v1.36.0
github.com/awslabs/aws-lambda-go-api-proxy v0.9.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.6.3
github.com/google/uuid v1.1.2
github.com/lestrrat-go/jwx v0.9.2
github.com/onsi/ginkgo v1.14.2 // indirect
github.com/onsi/gomega v1.10.3 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
Here's an example using github.com/golang-jwt/jwt, (formally known as github.com/dgrijalva/jwt-go,) and a JWKs like the one AWS Cognito provides.
It'll refresh the AWS Cognito JWKs once every hour, refresh when a JWT signed with an unknown kid comes in, and have a global rate limit of 1 HTTP request to refresh the JWKs every 5 minutes.
package main
import (
"fmt"
"log"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/MicahParks/keyfunc"
)
func main() {
// Get the JWKS URL from your AWS region and userPoolId.
//
// See the AWS docs here:
// https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
regionID := "" // TODO Get the region ID for your AWS Cognito instance.
userPoolID := "" // TODO Get the user pool ID of your AWS Cognito instance.
jwksURL := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", regionID, userPoolID)
// Create the keyfunc options. Use an error handler that logs. Refresh the JWKS when a JWT signed by an unknown KID
// is found or at the specified interval. Rate limit these refreshes. Timeout the initial JWKS refresh request after
// 10 seconds. This timeout is also used to create the initial context.Context for keyfunc.Get.
options := keyfunc.Options{
RefreshErrorHandler: func(err error) {
log.Printf("There was an error with the jwt.Keyfunc\nError: %s", err.Error())
},
RefreshInterval: time.Hour,
RefreshRateLimit: time.Minute * 5,
RefreshTimeout: time.Second * 10,
RefreshUnknownKID: true,
}
// Create the JWKS from the resource at the given URL.
jwks, err := keyfunc.Get(jwksURL, options)
if err != nil {
log.Fatalf("Failed to create JWKS from resource at the given URL.\nError: %s", err.Error())
}
// Get a JWT to parse.
jwtB64 := "eyJraWQiOiJmNTVkOWE0ZSIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJLZXNoYSIsImF1ZCI6IlRhc2h1YW4iLCJpc3MiOiJqd2tzLXNlcnZpY2UuYXBwc3BvdC5jb20iLCJleHAiOjE2MTkwMjUyMTEsImlhdCI6MTYxOTAyNTE3NywianRpIjoiMWY3MTgwNzAtZTBiOC00OGNmLTlmMDItMGE1M2ZiZWNhYWQwIn0.vetsI8W0c4Z-bs2YCVcPb9HsBm1BrMhxTBSQto1koG_lV-2nHwksz8vMuk7J7Q1sMa7WUkXxgthqu9RGVgtGO2xor6Ub0WBhZfIlFeaRGd6ZZKiapb-ASNK7EyRIeX20htRf9MzFGwpWjtrS5NIGvn1a7_x9WcXU9hlnkXaAWBTUJ2H73UbjDdVtlKFZGWM5VGANY4VG7gSMaJqCIKMxRPn2jnYbvPIYz81sjjbd-sc2-ePRjso7Rk6s382YdOm-lDUDl2APE-gqkLWdOJcj68fc6EBIociradX_ADytj-JYEI6v0-zI-8jSckYIGTUF5wjamcDfF5qyKpjsmdrZJA"
// Parse the JWT.
token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
if err != nil {
log.Fatalf("Failed to parse the JWT.\nError: %s", err.Error())
}
// Check if the token is valid.
if !token.Valid {
log.Fatalf("The token is not valid.")
}
log.Println("The token is valid.")
// End the background refresh goroutine when it's no longer needed.
jwks.EndBackground()
}
I'm following the tutorial https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-lambda-resolvers.html
And have some doubts for using just a switch to handle graphql queries.
Is there a better approach to handle more complicated requests?
The choice is yours as to how to setup lambda within your AppSync API. It is entirely reasonable to have a lambda function per resolver and have a function be responsible for a single resolver. You can alternatively take an approach like the tutorial and use a single function and some lightweight routing code to take care of calling the correct function. Using a single function can often offer some performance benefits because of how lambda's container warming works (esp. for Java & C# where VM startup time can add up) but has less separation of concerns.
Here are some approaches I have taken in the past:
Option 1: JS
This approach uses JavaScript and should feel familiar to those who have run their own GraphQL servers before.
const Resolvers = {
Query: {
me: (source, args, identity) => getLoggedInUser(args, identity)
},
Mutation: {
login: (source, args, identity) => loginUser(args, identity)
}
}
exports.handler = (event, context, callback) => {
// We are going to wire up the resolver to give all this information in this format.
const { TypeName, FieldName, Identity, Arguments, Source } = event
const typeResolver = Resolvers[TypeName]
if (!typeResolver) {
return callback(new Error(`No resolvers found for type: "${TypeName}"`))
}
const fieldResolver = typeResolver[FieldName]
if (!fieldResolver) {
return callback(new Error(`No resolvers found for field: "${FieldName}" on type: "${TypeName}"`), null)
}
// Handle promises as necessary.
const result = fieldResolver(Source, Arguments, Identity);
return callback(null, result)
};
You can then use a standard lambda resolver from AppSync. For now we have to provide the TypeName and FieldName manually.
#**
The value of 'payload' after the template has been evaluated
will be passed as the event to AWS Lambda.
*#
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"TypeName": "Query",
"FieldName": "me",
"Arguments": $util.toJson($context.arguments),
"Identity": $util.toJson($context.identity),
"Source": $util.toJson($context.source)
}
}
Option 2: Go
For the curious, I have also used go lambda functions successfully with AppSync. Here is one approach that has worked well for me.
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
"github.com/fatih/structs"
"github.com/mitchellh/mapstructure"
)
type GraphQLPayload struct {
TypeName string `json:"TypeName"`
FieldName string `json:"FieldName"`
Arguments map[string]interface{} `json:"Arguments"`
Source map[string]interface{} `json:"Source"`
Identity map[string]interface{} `json:"Identity"`
}
type ResolverFunction func(source, args, identity map[string]interface{}) (data map[string]interface{}, err error)
type TypeResolverMap = map[string]ResolverFunction
type SchemaResolverMap = map[string]TypeResolverMap
func resolverMap() SchemaResolverMap {
return map[string]TypeResolverMap{
"Query": map[string]ResolverFunction{
"me": getLoggedInUser,
},
}
}
func Handler(ctx context.Context, event GraphQLPayload) (map[string]interface{}, error) {
// Almost the same as the JS option.
resolvers := resolverMap()
typeResolver := resolvers[event.TypeName]
if typeResolver == nil {
return nil, fmt.Errorf("No type resolver for type " + event.TypeName)
}
fieldResolver := typeResolver[event.FieldName]
if fieldResolver == nil {
return nil, fmt.Errorf("No field resolver for field " + event.FieldName)
}
return fieldResolver(event.Source, event.Arguments, event.Identity)
}
func main() {
lambda.Start(Handler)
}
/**
* Resolver Functions
*/
/**
* Get the logged in user
*/
func getLoggedInUser(source, args, identity map[string]interface{}) (data map[string]interface{}, err error) {
// Decode the map[string]interface{} into a struct I defined
var typedArgs myModelPackage.GetLoggedInUserArgs
err = mapstructure.Decode(args, &typedArgs)
if err != nil {
return nil, err
}
// ... do work
res, err := auth.GetLoggedInUser()
if err != nil {
return nil, err
}
// Map the struct back to a map[string]interface{}
return structs.Map(out), nil
}
// ... Add as many more as needed
You can then use the same resolver template as used in option 1. There are many other ways to do this but this is one method that has worked well for me.
Hope this helps :)
You are not forced to use one single AWS Lambda to handle each request. For this tutorial it's easier for newcomers to get the idea of it, therefore they used this approach.
But it's up to you how to implement it in the end. An alternative would be to create for each resolver a separate AWS Lambda to eliminate the switch and to follow Single Responsibility Principle (SRP).
You can proxy all the queries to a graphql-server
Apollo GraphQL Server provides a very good setup to deploy a GraphQL server in AWS Lambda.
So I have this Client struct that has a method UserByID that makes a HTTP request to an endpoint for a User. I want to unit test this function but also not make an actual HTTP request in the function c.Request. I want to stub that function with a response and error I can control.
func (c Client) UserByID(id string) (u User, err error) {
v := url.Values{}
v.Set("id", id)
opts := Request{
HTTP: http.Request{
Method: http.MethodGet,
Form: v,
},
URL: 'some/endpoint/users',
}
resp, err := c.Request(opts)
err = json.Unmarshal(resp, &u)
return
}
Here's what the stub looks like:
type mockClient struct {
Client
fakeUser User
fakeError error
}
func (mc mockClient) Request(opts Request) (resp []byte, err error) {
resp, err = json.Marshal(mc.fakeUser)
err = mc.fakeError
return
}
In a single test I have something like:
client := mockClient{
fakeUser: User{},
fakeError: nil,
}
user, err := client.UserByID(c.id)
Then I can assert the return values from client.UserByID. In this example I'm trying to override the client.Request function but I understand Go is not an inheritance-type of language. In my tests, my mockClient.Request function is not being called. The original client.Request is still being called.
I then assume that my approach is not right then. How can I test client.UserByID without actually calling the real client.Request function within it? Should the design of my methods be different?
To accomplish what you need, you can re-structure your code just a little bit.
You can find a full working example here: https://play.golang.org/p/VoO4M4U0YcA
And below is the explanation.
First, declare a variable function in your package to encapsulate the actual making of the HTTP request:
var MakeRequest = func(opts Request) (resp []byte, err error) {
// make the request, return response and error, etc
}
Then, in your Client use that function to make the request:
func (c Client) Request(opts Request) (resp []byte, err error) {
return MakeRequest(opts)
}
In that way, when you actually use the client, it will make the HTTP request as expected.
But then when you need to test, you can assign a mock function to that MakeRequest function so that you can control its behaviour:
// define a mock requester for your test
type mockRequester struct {
fakeUser User
fakeError error
}
func (mc mockRequester) Request(opts Request) (resp []byte, err error) {
resp, err = json.Marshal(mc.fakeUser)
err = mc.fakeError
return
}
// to use it, you can just point `MakeRequest` to the mock object function
mockRequester := mockRequester{
fakeUser: User{ ID: "fake" },
fakeError: nil,
}
MakeRequest = mockRequester.Request
I then assume that my approach is not right then.
Your description covers it Exactly! Even though you're embedding the Client in mockClient when you call client.UserByID(c.id) go looks at the mockClient and sees the method pulled up from Client . it ends up so that the Client!!! is the receiver to UserByID call NOT the mockClient. You can see this here:
func (c Client) UserByID(id string) (u User, err error)
Once the Client is the receiver resp, err := c.Request(opts) is called with the Client receiver above and NOT your mockClient as you're observing.
One way to introduce a seam for c.Request that you can provide a custom implementation for use in unit testing is to make Request a callout method on your Client struct.
type Client struct {
Request func(opts Request) (resp []byte, err error)
}
The above should help to decouple Client from Request implementation. All it says is that Request will be a function that takes some args with some return value, allowing you to substitute different functions depending if you're in production or testing. Now during your public initialization of Client you can provide your real implementation of Request, while in unit tests you can provide your fake implementation.
type mockRequester struct {
fakeUser User
fakeError error
}
func (mc mockRequester) Request(opts Request) (resp []byte, err error) {
resp, err = json.Marshal(mc.fakeUser)
err = mc.fakeError
return
}
mr := mockRequester{...}
c := Client{
Request: mr.Request,
}
This comes with its own tradeoffs though as you potentially lose the client as a pointer receiver in your Request callout function.
Another cool part of the Callout is that it gives you another option of encapsulation. Suppose in the future you'd like to provide some sort of exponential backoff or retry. It would allow you to provide a more intelligent Request method to Client without Client having to change.
I'm writing an appengine app in Go that uses Google cloud storage.
For example, my "reading" code looks like:
client, err := storage.NewClient(ctx)
if err != nil {
return nil, err
}
defer func() {
if err := client.Close(); err != nil {
panic(err)
}
}()
r, err := client.Bucket(BucketName).Object(id).NewReader(ctx)
if err != nil {
return nil, err
}
defer r.Close()
return ioutil.ReadAll(r)
... where ctx is a context from appengine.
When I run this code in a unit test (using aetest), it actually sends requests to my cloud storage; I'd like to run this hermetically instead, similar to how aetest allows fake datastore calls.
(Possibly related question, but it deals with python, and the linked github issue indicates it's solved in a python-specific way).
How can I do this?
One approach, also suggested here is to allow your GCS client to have its downloader swapped out for a stub while unit testing. First, define an interface that matches how you use the Google Cloud Storage library, and then reimplement it with fake data in your unit tests.
Something like this:
type StorageClient interface {
Bucket(string) Bucket // ... and so on, matching the Google library
}
type Storage struct {
client StorageClient
}
// New creates a new Storage client
// This is the function you use in your app
func New() Storage {
return NewWithClient(&realGoogleClient{}) // provide real implementation here as argument
}
// NewWithClient creates a new Storage client with a custom implementation
// This is the function you use in your unit tests
func NewWithClient(client StorageClient) {
return Storage{
client: client,
}
}
It can be a lot of boilerplate to mock entire 3rd party APIs, so maybe you'll be able to make it easier by generating some of those mocks with golang/mock or mockery.
I have done something like this...
Since storage client is sending HTTPS request so I mocked the HTTPS server using httptest
func Test_StorageClient(t *testing.T) {
tests := []struct {
name string
mockHandler func() http.Handler
wantErr bool
}{
{
name: "test1",
mockHandler: func() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("22\n96\n120\n"))
return
})
},
wantErr: false,
},
{
name: "test2 ",
mockHandler: func() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
return
})
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serv := httptest.NewTLSServer(tt.mockHandler())
httpclient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
client, _ := storage.NewClient(context.Background(), option.WithEndpoint(serv.URL), option.WithoutAuthentication(), option.WithHTTPClient(&httpclient))
got, err := readFileFromGCS(client)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
Cloud Storage on the Python Development server is emulated using local files with the Blobstore service, which is why the solution of using a Blobstore stub with testbed (also Python-specific) worked. However there is no such local emulation for Cloud Storage on the Go runtime.
As Sachin suggested, the way to unit test Cloud Storage is to use a mock. This is the way it's done internally and on other runtimes, such as node.
I would advice you reduce the mocks as much as possible you might need to use an hermetic approach to make it almost similar to the real thing .
https://testing.googleblog.com/2012/10/hermetic-servers.html