// Copyright 2021 Tencent Inc. All rights reserved. package core_test import ( "bytes" "context" "crypto" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "io" "io/ioutil" "math/rand" "mime/multipart" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" "time" "git.nanodreamtech.com/sg/wechatpay-go/core" "git.nanodreamtech.com/sg/wechatpay-go/core/auth" "git.nanodreamtech.com/sg/wechatpay-go/core/auth/signers" "git.nanodreamtech.com/sg/wechatpay-go/core/auth/verifiers" "git.nanodreamtech.com/sg/wechatpay-go/core/option" "git.nanodreamtech.com/sg/wechatpay-go/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( testMchID = "example-mchid" testCertificateSerialNumber = "example-sn" // NOTE: 以下是随机生成的测试密钥,请勿用于生产环境 testPrivateKey = "-----BEGIN TESTING KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkxOav8p5RFFmN\n7hjLGrNtXPYgCd0Zuvxabv+IWl1HVkWi/1iVqac+XwKH/ZeCYDURqZ6P0iiq8NBd\npygoeJiQM+qzaTV3alNXdLcpcaQSNqJ16a7Z2Co5LgOkBJHIF1qUWf+BpAtyLqo5\niGUQ47w3IWQHtfpW7RrECiNsI7kGKuSc4U2JX/gxrFG7ugpRA9Gp1eF0/wBMWSKV\nmKveKERvAueaTKEujpN4lctoO1wsMW93nuFNH1gtHPYmkaZaJS88GEp0VYJIcOpX\n2HlVPPiWx+0KAndcqMLVQ+qTk21tivpjuqPDstxcT9cXn/3CzSEkYrKkMLpjpSl/\nIn+amHddAgMBAAECggEADtQRlsAU82MLdDR7UrwCbdMx60w387raPyFCKflH78WZ\n2sN0K3PrMzfFuItf+UHDROWo+XSGaGvntKX4fTvtLv0dICxVvXt6KKK+YSJzC5iT\nIl13eO91TVQQy9AFdqZzZmp7DiW/SfVdKHRX9B8qryN4JyF/eBc6k23+JhtI6X8J\nrPeXGcw/Muo2cvoZ6oarRvzKU7pDivZADavAsgGwi+QwZUtrpUhDbJxWxoasCrWz\n6X24D+JbKndf/AeyHC2mqoAwTYdQgkkPHLJWGsqAt/GHtHLmZHIPc0dXfksP5omX\nOIii34YNud1j2/X0ryxoRBKPGIolV5Tyyh9PX75UAQKBgQDa/oPFUNsBo+NkfE4r\nMry+mnAeBM06vZh65acbHu3prqzn7tzfQR5rF9nIEUjNkach+ZeTnUxKfX4TwfiL\nibbM1Epb+yhEpSlA9HhY1AGhdNKpFbq63oVzS+lwpmLcXFLHhOYw8GrnYH9lcE+E\nYCqFcuk4t/y3rU/8Y5GhZKZ53QKBgQDAnKk6CEgnSkXfZLmzVtoM/5hnI0GEDOUo\nV35alqvgdJtCiPs4C03snYLVHqHjAknGzLGONAQ6h9au2qwcHy1qH/noq151u92b\nbCrKmghnm2SoIgCaZ7i2scWm6NM9Da60H662WxjaKcZMnUClm+G+Irl9m5cm1i3V\n56ZU63nbgQKBgHyAlFO6mzg8f4via+J9TvciADngyvjpT2YXaECv/dyL9TtK/oFi\nmTOTdLocsYJFm3piVv2SQQxcejArZ+2U1rtuufO/P25/Y4vNMRp3NZIgQ5/jfay9\n06rv7oCf57aWOm26LdCG7pAquWLnTh3ZOnNyGAup9mBKhR3dUa8q9MZ1AoGATZGJ\n0VYugKw3sXymEKRkkiGJJdgb9WsgCnwZ5a+SLoWnVUdHLM3YpvbUDrIUbhCo14ft\n5Z/rKAs2mRp1f6nKp1eTVHFXTEDJQWNxZEBeLCN3iQKQjZ5B1EmJmOtgztCoz9+G\ng+fx/UIfmxElTMyXP/RKEVzMpZZRxThSUxa174ECgYEAsviAmgBskM2ibtrA8tNW\njHdgut0xAtIJIIHlYmtksWgAWD4cPtCg7HPurXqBKxMogH/ZsZc6/5PpOIRYBNl0\nEebH0MZ/yiEOrmgsFJ1gKWk3fx8/yLQBlhn32AIhj7wmcFwzi/4hcwihHRCjS7t5\nQhVpKswxQyxqeIdQw0CgKZY=\n-----END TESTING KEY-----" testWechatPrivateKeyStr = "-----BEGIN TESTING KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDzbMaiMbCzJ11sZ2r7/XisolGu1pVpWvnv1PVOAWZZAWr/WcNNuLni8ddJkIs8NdTz+iAHtb9XMNG4hj7d10cy8QE6QG8YUef1fGb47Wee3DjJRk8N9lWyPDAy9AW70yYWItl/05XgkGt2eJuoU5CcZ+Cy0u2nAXxEGs2Z4Fg/100Ylcyq4GrimjngIyUFLnowOcZltJUQSw/Iu63V0BEh9PMNnXhKkwS2xfkA2nZRzSgzpMvX74v8F7Zf+HkyrHHzwY/7YUWg3pSj3GD0xKsJOwzz0MLlS8uIdLj1lKGzzt1ROgwe0sM5LL5XMfmbjDhcVBmyxQI80WiNaC281tVhAgMBAAECggEBAJ134Wrs0Ayky2ej4u5OAvFSM5rxj0fPJV3DGkiy2R18sFWtII03kXBA1+7rxVZW0IJfbLbwGG3z08cVeLeTWqiWhR/ErNlDqtT/+7DOCrkWZtm1VNCIaNla3Ccp+keNiNbLBn4NRqg1ZH8H+FHEdQjonc+waTIe4N9Bo30GRrBMeMbAgN8mwQhZ+6R23j3GsrJOViFpPRgGhih4aEAORxU+DWl22vklxO8lnDzSwfnXvNDvapnPaA8VXekkThxABq3p/ggv5MI1QPYl8BU5PWd6AJlPs/u2nzFGmCgVufHMjdszWYd3hbj5EmhlL1VMs4uxhCC8OM+ypbnx0CmBB5ECgYEA+W3KJciw4qG6XqWgjK9hkgiZrO8z3tu6tQ6ge28f3BxYEbGUab8bKKzacbYODRiRCM5oKcrXfTqP6IbqUoESiuoz0CUPbShp7k00wXKd7BH8LoIECDbctz77NG0KBE31OGEJw4hm762M14V9nU0KDHGuud1CH1fqivTGG0g2HiUCgYEA+dZ+e5iSvin+omXkr3Vhwf3kutX+GNkKm5LrWZNmWybOKw/K1YFvESpfl1b6YgjA/qUXj0tbOumFQw6e/OLfxIvK/dtkd+7pbSC4T9w8rH7zgjYdJ030Nyv3UGfHDbES+z9MDMo5h+3RKsLN2bp1JcXp6vht9CiXDYm1df3O340CgYB118Qg08+WU1iM7O2MajPL3dpVFPJJwUBV2GJDzv2bbZzCR0baKxr2vau6+4tp7ohfQ718uUPT+34QGuXMMwUCsqHmHgxKw0RA/SMGnlM0PE8L3gtvohPnU481dqq72+UWTOpjAie35yPak0wErGgp9u/ZCkr6Kfw6yGhsbVJ8LQKBgEBLxS1FrK4n3JIqqtnE2a21C4JRxBzc7m/vNYZN+s+GgxRt8gNUViMSxpsKFVHZcuGV1yRXflkA8/y37I6kTHYmi80dAxQidgxRmV1kDnFOEpj2GDafRzRTqkgVDRMm+P2T4pyABqJGv8fDbnqUE8Xu0y5XVOS69XTUddCxyuWZAoGAbpF6JOh6B7OV4XRTDm98Z8OPYmYd9JQ4xt8bqsG9LdzvhU/PI4zwIaKDqZ8vzCI+r8TOrC6SBEfjAe6o2FEExmFWTjBAVCp+Qvnz+Pj7d+WP3kCX/B62IZckVhdV3a2frMTBPvAh8XdENdvsu4DsWJBCA54GLU3wdUa/FO0RUsA=\n-----END TESTING KEY-----\n" testWechatCertificateStr = "-----BEGIN CERTIFICATE-----\nMIID1TCCAr2gAwIBAgIRAJ8qZJYAQUwUheimQ8sQNZMwDQYJKoZIhvcNAQELBQAw\nXjELMAkGA1UEBhMCQ04xDjAMBgNVBAoTBU15U1NMMSswKQYDVQQLEyJNeVNTTCBU\nZXN0IFJTQSAtIEZvciB0ZXN0IHVzZSBvbmx5MRIwEAYDVQQDEwlNeVNTTC5jb20w\nHhcNMjEwNjI3MTMwNTM2WhcNMjIwNjI3MTMwNTM2WjAkMQswCQYDVQQGEwJDTjEV\nMBMGA1UEAxMMd2VjaGF0cGF5LWdvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA82zGojGwsyddbGdq+/14rKJRrtaVaVr579T1TgFmWQFq/1nDTbi54vHX\nSZCLPDXU8/ogB7W/VzDRuIY+3ddHMvEBOkBvGFHn9Xxm+O1nntw4yUZPDfZVsjww\nMvQFu9MmFiLZf9OV4JBrdnibqFOQnGfgstLtpwF8RBrNmeBYP9dNGJXMquBq4po5\n4CMlBS56MDnGZbSVEEsPyLut1dARIfTzDZ14SpMEtsX5ANp2Uc0oM6TL1++L/Be2\nX/h5Mqxx88GP+2FFoN6Uo9xg9MSrCTsM89DC5UvLiHS49ZShs87dUToMHtLDOSy+\nVzH5m4w4XFQZssUCPNFojWgtvNbVYQIDAQABo4HHMIHEMA4GA1UdDwEB/wQEAwIH\ngDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBQogSYF0TQaP8FzD7uT\nzxUcPwO/fzBjBggrBgEFBQcBAQRXMFUwIQYIKwYBBQUHMAGGFWh0dHA6Ly9vY3Nw\nLm15c3NsLmNvbTAwBggrBgEFBQcwAoYkaHR0cDovL2NhLm15c3NsLmNvbS9teXNz\nbHRlc3Ryc2EuY3J0MBcGA1UdEQQQMA6CDHdlY2hhdHBheS1nbzANBgkqhkiG9w0B\nAQsFAAOCAQEAjh4oxMcJqsVaN5/aA+4+NSfV9wR4uzTVtAyL/dApymZn6Wjknd85\nDltekcTflNP84bDiFEE3Ls3RYatjRx9pWeW7QbpdYvfDtWuxL5dhzRYtUO83z8wT\n+/sceeyNOQAWGD6Gt7Aw7yb7bIZ5slcZYepqdKSHyMnn06CCNRZtDVTuQYRqnmoh\nCaK5RNe4lYM/hncMgddE/DugTxzh5NUMpAY4xAsqOofkVmptX3trVZVILPglJ6nQ\n5dALCpp2UCuxikwFdEpvvGIC2qZQv5jmemFLCDIQZ227GUZ/EcbuTtAdQYHUnGJT\nvrFBSDe4FbKwljUmrccD/LkR9FmPn6gWKA==\n-----END CERTIFICATE-----\n" fileName = "picture.jpeg" responseBody = `{"hello":"client"}` testRequestUri = "/v3/resource?first=this+is+a+field&second=was+it+clear+%28already%29%3F" ) var ( privateKey *rsa.PrivateKey wechatPayPrivateKey *rsa.PrivateKey wechatPayCertificate *x509.Certificate signer auth.Signer verifier auth.Verifier ctx context.Context ) type signParameter map[string]string func init() { ctx = context.Background() var err error privateKey, err = utils.LoadPrivateKey(testingKey(testPrivateKey)) if err != nil { panic(fmt.Errorf("load merchant testing key err:%s", err.Error())) } wechatPayCertificate, err = utils.LoadCertificate(testWechatCertificateStr) if err != nil { panic(fmt.Errorf("generate wechatpay testing certificate err:%s", err.Error())) } wechatPayPrivateKey, err = utils.LoadPrivateKey(testingKey(testWechatPrivateKeyStr)) if err != nil { panic(fmt.Errorf("generate wechatpay testing key err:%s", err.Error())) } } func writeResponse(w http.ResponseWriter) { writeSignature(w, responseBody) w.WriteHeader(http.StatusOK) fmt.Fprint(w, responseBody) } func writeSignature(w http.ResponseWriter, body string) { w.Header().Set("Request-Id", "0") w.Header().Set("Wechatpay-Serial", utils.GetCertificateSerialNumber(*wechatPayCertificate)) nonce := "this-is-a-nonce" w.Header().Set("Wechatpay-Nonce", nonce) timestamp := strconv.FormatInt(time.Now().Unix(), 10) w.Header().Set("Wechatpay-Timestamp", timestamp) signature, _ := utils.SignSHA256WithRSA( fmt.Sprintf("%s\n%s\n%s\n", timestamp, nonce, body), wechatPayPrivateKey) w.Header().Set("Wechatpay-Signature", signature) } func parseAuthorization(t *testing.T, authorization string) (schema string, param signParameter) { m := make(signParameter) s1 := strings.Split(authorization, " ") assert.Equal(t, len(s1), 2) s2 := strings.Split(s1[1], ",") assert.Equal(t, len(s2), 5) for _, v := range s2 { s3 := strings.SplitN(v, "=", 2) assert.Equal(t, len(s3), 2) pk := s3[0] pv := strings.Trim(s3[1], "\"") m[pk] = pv } return s1[0], m } func assertAuthorization(t *testing.T, schema, method, uri string, params signParameter, body []byte) { assert.Equal(t, schema, "WECHATPAY2-SHA256-RSA2048") assert.Equal(t, params["mchid"], testMchID) assert.Equal(t, params["serial_no"], testCertificateSerialNumber) message := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n", method, uri, params["timestamp"], params["nonce_str"], body) hashed := sha256.Sum256([]byte(message)) signBytes, err := base64.StdEncoding.DecodeString(params["signature"]) assert.NoError(t, err) assert.NoError(t, rsa.VerifyPKCS1v15(&privateKey.PublicKey, crypto.SHA256, hashed[:], signBytes)) } func TestGet(t *testing.T) { opts := []core.ClientOption{ option.WithMerchantCredential(testMchID, testCertificateSerialNumber, privateKey), option.WithWechatPayCertificate([]*x509.Certificate{wechatPayCertificate}), } client, err := core.NewClient(ctx, opts...) require.NoError(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, "GET") assert.Equal(t, r.RequestURI, testRequestUri) schema, params := parseAuthorization(t, r.Header.Get("Authorization")) assertAuthorization(t, schema, r.Method, r.RequestURI, params, make([]byte, 0)) writeResponse(w) })) defer ts.Close() result, err := client.Get(ctx, ts.URL+testRequestUri) assert.NoError(t, err) body, err := ioutil.ReadAll(result.Response.Body) assert.NoError(t, err) assert.Equal(t, string(body), responseBody) } type testData struct { StockID string `json:"stock_id"` StockCreatorMchID string `json:"stock_creator_mchid"` OutRequestNo string `json:"out_request_no"` AppID string `json:"appid"` } func TestPost(t *testing.T) { opts := []core.ClientOption{ option.WithMerchantCredential(testMchID, testCertificateSerialNumber, privateKey), option.WithWechatPayCertificate([]*x509.Certificate{wechatPayCertificate}), } client, err := core.NewClient(ctx, opts...) require.NoError(t, err) data := &testData{ StockID: "xxx", StockCreatorMchID: "xxx", OutRequestNo: "xxx", AppID: "xxx", } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, "POST") assert.Equal(t, r.RequestURI, testRequestUri) schema, params := parseAuthorization(t, r.Header.Get("Authorization")) body, _ := ioutil.ReadAll(r.Body) assertAuthorization(t, schema, r.Method, r.RequestURI, params, body) writeResponse(w) })) defer ts.Close() result, err := client.Post(ctx, ts.URL+testRequestUri, data) assert.NoError(t, err) body, err := ioutil.ReadAll(result.Response.Body) assert.NoError(t, err) assert.Equal(t, string(body), responseBody) } func TestRequest(t *testing.T) { opts := []core.ClientOption{ option.WithMerchantCredential(testMchID, testCertificateSerialNumber, privateKey), option.WithWechatPayCertificate([]*x509.Certificate{wechatPayCertificate}), } client, err := core.NewClient(ctx, opts...) require.NoError(t, err) data := &testData{ StockID: "xxx", StockCreatorMchID: "xxx", OutRequestNo: "xxx", AppID: "xxx", } tt := []struct { method string uri string contentType string body interface{} header http.Header }{ { http.MethodGet, "/v3/get", "", nil, http.Header{"My-Id": {"1234"}}, }, { http.MethodPost, testRequestUri, "application/json", data, http.Header{"My-Id": {"1234"}}, }, { http.MethodDelete, "/v3/delete", "", nil, nil, }, { http.MethodPut, "/v3/put", "", data, http.Header{"My-Id": {"1234"}}, }, { http.MethodPatch, "/v3/patch", "", data, nil, }, } for _, test := range tt { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, test.method, r.Method) assert.Equal(t, test.uri, r.RequestURI) if test.header != nil { assert.Equal(t, "1234", r.Header.Get("My-Id")) } schema, params := parseAuthorization(t, r.Header.Get("Authorization")) body, _ := ioutil.ReadAll(r.Body) assertAuthorization(t, schema, r.Method, r.RequestURI, params, body) assert.Equal(t, "9F2A649600414C1485E8A643CB103593", r.Header.Get("Wechatpay-Serial")) if test.body != nil { assert.Equal(t, "application/json", r.Header.Get("Content-Type")) var req testData err = json.Unmarshal(body, &req) assert.NoError(t, err) assert.Equal(t, data, &req) } writeResponse(w) })) testUrl, err := url.Parse(ts.URL + test.uri) assert.NoError(t, err) result, err := client.Request( ctx, test.method, ts.URL+testUrl.Path, test.header, testUrl.Query(), test.body, test.contentType, ) assert.NoError(t, err) body, err := ioutil.ReadAll(result.Response.Body) assert.NoError(t, err) assert.Equal(t, http.StatusOK, result.Response.StatusCode) assert.Equal(t, responseBody, string(body)) ts.Close() } } func TestClientVerifyFail(t *testing.T) { opts := []core.ClientOption{ option.WithMerchantCredential(testMchID, testCertificateSerialNumber, privateKey), option.WithWechatPayCertificate([]*x509.Certificate{wechatPayCertificate}), } client, err := core.NewClient(ctx, opts...) require.NoError(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Request-Id", "0") w.Header().Set("Wechatpay-Serial", utils.GetCertificateSerialNumber(*wechatPayCertificate)) nonce := "this-is-a-nonce" w.Header().Set("Wechatpay-Nonce", nonce) timestamp := strconv.FormatInt(time.Now().Unix(), 10) w.Header().Set("Wechatpay-Timestamp", timestamp) w.Header().Set("Wechatpay-Signature", "AABB") w.WriteHeader(http.StatusOK) })) defer ts.Close() _, err = client.Get(ctx, ts.URL+testRequestUri) assert.Contains(t, err.Error(), "verify fail") } func TestClientNoAuth(t *testing.T) { opts := []core.ClientOption{ option.WithMerchantCredential(testMchID, testCertificateSerialNumber, privateKey), option.WithWechatPayCertificate([]*x509.Certificate{wechatPayCertificate}), } client, err := core.NewClient(ctx, opts...) require.NoError(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(401) w.Header().Set("Request-Id", "0") fmt.Fprint(w, `{"code":"SIGN_ERROR","message":"sign error"}`) })) defer ts.Close() _, err = client.Get(ctx, ts.URL+testRequestUri) apiError, ok := err.(*core.APIError) assert.True(t, ok) assert.Equal(t, 401, apiError.StatusCode) assert.Equal(t, "SIGN_ERROR", apiError.Code) assert.Equal(t, "sign error", apiError.Message) } type meta struct { FileName string `json:"filename" binding:"required"` // 商户上传的媒体图片的名称,商户自定义,必须以JPG、BMP、PNG为后缀。 Sha256 string `json:"sha256" binding:"required"` // 图片文件的文件摘要,即对图片文件的二进制内容进行sha256计算得到的值。 } func TestClient_Upload(t *testing.T) { // 如果你有自定义的Signer或者Verifier signer = &signers.SHA256WithRSASigner{ MchID: testMchID, PrivateKey: privateKey, CertificateSerialNo: testCertificateSerialNumber, } verifier = verifiers.NewSHA256WithRSAVerifier( core.NewCertificateMap( map[string]*x509.Certificate{utils.GetCertificateSerialNumber(*wechatPayCertificate): wechatPayCertificate}, ), ) client, err := core.NewClient(ctx, option.WithSigner(signer), option.WithVerifier(verifier)) require.NoError(t, err) pictureBytes := make([]byte, 1024) // 随机的数据充当图片数据 rand.Read(pictureBytes) // 计算文件序列化后的sha256 h := sha256.New() _, err = h.Write(pictureBytes) assert.NoError(t, err) metaObject := &meta{} pictureSha256 := h.Sum(nil) metaObject.FileName = fileName metaObject.Sha256 = fmt.Sprintf("%x", string(pictureSha256)) metaByte, _ := json.Marshal(metaObject) reqBody := &bytes.Buffer{} writer := multipart.NewWriter(reqBody) err = core.CreateFormField(writer, "meta", "application/json", metaByte) assert.NoError(t, err) err = core.CreateFormFile(writer, fileName, "image/jpg", pictureBytes) assert.NoError(t, err) err = writer.Close() assert.NoError(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, "POST") assert.Equal(t, r.RequestURI, testRequestUri) mr, err := r.MultipartReader() assert.NoError(t, err) var body []byte for { p, err := mr.NextPart() if err == io.EOF { break } assert.NoError(t, err) if p.FormName() == "meta" { body, _ = ioutil.ReadAll(p) assert.Equal(t, metaByte, body) } else if p.FormName() == "file" { slurp, _ := ioutil.ReadAll(p) assert.Equal(t, pictureBytes, slurp) } } schema, params := parseAuthorization(t, r.Header.Get("Authorization")) assertAuthorization(t, schema, r.Method, r.RequestURI, params, body) writeResponse(w) })) defer ts.Close() result, err := client.Upload( ctx, ts.URL+testRequestUri, string(metaByte), reqBody.String(), writer.FormDataContentType()) assert.NoError(t, err) if result.Response.Body != nil { defer result.Response.Body.Close() } body, err := ioutil.ReadAll(result.Response.Body) assert.NoError(t, err) t.Log(string(body)) assert.Equal(t, string(body), responseBody) } func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }