commit 1b46dc08ebce5ea8fbe6bfeaa17a7348517d986e
parent 6678d6a01acd7c130d546a3a3ecc8250b7d316d9
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Fri, 25 Apr 2025 15:12:33 +0200
add payment tests
Diffstat:
4 files changed, 248 insertions(+), 150 deletions(-)
diff --git a/cmd/mailbox-server/main_test.go b/cmd/mailbox-server/main_test.go
@@ -7,12 +7,16 @@ import (
"crypto/sha512"
"encoding/binary"
"encoding/json"
+ "fmt"
"net/http"
"net/http/httptest"
"os"
+ "strings"
"testing"
gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util"
+ "github.com/schanzen/taler-go/pkg/merchant"
+ "github.com/schanzen/taler-go/pkg/util"
"taler.net/taler-mailbox/internal/gana"
"taler.net/taler-mailbox/pkg/rest"
)
@@ -23,6 +27,32 @@ var testWalletAlicePriv ed25519.PrivateKey
var testWalletAlice ed25519.PublicKey
var testWalletAliceString string
+const merchantConfigResponse = `{
+ "currency": "KUDOS",
+ "currencies": {
+ "KUDOS": {
+ "name": "Kudos (Taler Demonstrator)",
+ "currency": "KUDOS",
+ "num_fractional_input_digits": 2,
+ "num_fractional_normal_digits": 2,
+ "num_fractional_trailing_zero_digits": 2,
+ "alt_unit_names": {
+ "0": "ク"
+ }
+ }
+ },
+ "exchanges": [
+ {
+ "master_pub": "F80MFRG8HVH6R9CQ47KRFQSJP3T6DBJ4K1D9B703RJY3Z39TBMJ0",
+ "currency": "KUDOS",
+ "base_url": "https://exchange.demo.taler.net/"
+ }
+ ],
+ "implementation": "urn:net:taler:specs:taler-merchant:c-reference",
+ "name": "taler-merchant",
+ "version": "18:0:15"
+}`
+
func executeRequest(req *http.Request) *httptest.ResponseRecorder {
rr := httptest.NewRecorder()
a.Router.ServeHTTP(rr, req)
@@ -41,6 +71,41 @@ func TestMain(m *testing.M) {
h := sha512.New()
h.Write(testWalletAlice)
testWalletAliceString = gnunetutil.EncodeBinaryToString(h.Sum(nil))
+
+ merchServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var orderResp merchant.PostOrderRequest
+ if r.URL.Path == "/config" {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(merchantConfigResponse))
+ return
+ }
+ if !strings.HasPrefix(r.URL.Path, "/private/orders") {
+ fmt.Printf("Expected to request '/private/orders', got: %s\n", r.URL.Path)
+ return
+ }
+ if r.Method == http.MethodPost {
+ err := json.NewDecoder(r.Body).Decode(&orderResp)
+ if err != nil {
+ fmt.Printf("Error %s\n", err)
+ }
+ jsonResp := fmt.Sprintf("{\"order_id\":\"%s\"}", orderResp.Order.OrderId)
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(jsonResp))
+ } else {
+ if r.Header.Get("PaidIndicator") == "yes" {
+ jsonResp := "{\"order_status\":\"paid\"}"
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(jsonResp))
+ } else {
+ jsonResp := "{\"order_status\":\"unpaid\", \"taler_pay_uri\": \"somepaytouri\"}"
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(jsonResp))
+ }
+ }
+ }))
+ defer merchServer.Close()
+ a.Merchant = merchant.NewMerchant(merchServer.URL, "")
+
code := m.Run()
// Purge DB
a.Db.Where("1 = 1").Delete(&mailbox.InboxEntry{})
@@ -74,6 +139,36 @@ func TestPostMessage(t *testing.T) {
}
}
+func TestPostMessagePaid(t *testing.T) {
+ testMessage := make([]byte, 256)
+
+ // Make paid
+ fee,err := util.ParseAmount("KUDOS:1")
+ if err != nil {
+ t.Errorf("%v", err)
+ }
+ a.MessageFee = fee
+ a.Db.Where("1 = 1").Delete(&mailbox.InboxEntry{})
+ req, _ := http.NewRequest("POST", "/" + testWalletAliceString, bytes.NewReader(testMessage))
+ response := executeRequest(req)
+
+ checkResponseCode(t, http.StatusPaymentRequired, response.Code)
+
+ // TODO check QR / payto response
+
+ req, _ = http.NewRequest("POST", "/" + testWalletAliceString, bytes.NewReader(testMessage))
+ req.Header.Add("PaidIndicator", "yes")
+ response = executeRequest(req)
+
+ checkResponseCode(t, http.StatusPaymentRequired, response.Code)
+
+ body := response.Body.String()
+ if body != "" {
+ t.Errorf("Expected empty response, Got %s", body)
+ }
+ a.MessageFee,_ = util.ParseAmount("KUDOS:0")
+}
+
func TestPostThenDeleteMessage(t *testing.T) {
// testMessage := make([]byte, 256)
var deletionReq mailbox.MessageDeletionRequest
@@ -114,11 +209,11 @@ func TestPostThenDeleteMessage(t *testing.T) {
for i := 0; i < 10; i++ {
h.Write(testMessages[i*256:(i+1)*256])
}
- var signed_msg [64+4+4]byte
+ var signed_msg [64+4+4]byte
size := signed_msg[0:4]
- binary.BigEndian.PutUint32(size, 64+4+4)
+ binary.BigEndian.PutUint32(size, 64+4+4)
purp := signed_msg[4:8]
- binary.BigEndian.PutUint32(purp, gana.TALER_SIGNATURE_PURPOSE_MAILBOX_MESSAGES_DELETE)
+ binary.BigEndian.PutUint32(purp, gana.TALER_SIGNATURE_PURPOSE_MAILBOX_MESSAGES_DELETE)
checksum := h.Sum(nil)
copy(signed_msg[8:], checksum)
sig := ed25519.Sign(testWalletAlicePriv, signed_msg[0:])
diff --git a/go.mod b/go.mod
@@ -5,7 +5,7 @@ go 1.18
require (
git.gnunet.org/gnunet-go.git v0.1.28-0.20220717050634-369422be2512
github.com/gorilla/mux v1.8.0
- github.com/schanzen/taler-go v1.0.6
+ github.com/schanzen/taler-go v1.0.8
gopkg.in/ini.v1 v1.66.6
gorm.io/driver/postgres v1.3.8
gorm.io/gorm v1.23.8
diff --git a/go.sum b/go.sum
@@ -104,8 +104,8 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
-github.com/schanzen/taler-go v1.0.6 h1:qf34AXE6aFBQkcJ3/V25apWcHLPv81JJgBVlSEUZaW0=
-github.com/schanzen/taler-go v1.0.6/go.mod h1:+l2TVAPZkF2d15X/XPLYZI5R6PdW6gc6Wft12jrl7tA=
+github.com/schanzen/taler-go v1.0.8 h1:vrFtYpE2hgfMrft/hzv3O1NjtjDOF0I44ossRw7pLT0=
+github.com/schanzen/taler-go v1.0.8/go.mod h1:+l2TVAPZkF2d15X/XPLYZI5R6PdW6gc6Wft12jrl7tA=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
diff --git a/pkg/rest/mailbox.go b/pkg/rest/mailbox.go
@@ -21,6 +21,7 @@ package mailbox
import (
"bytes"
"crypto/ed25519"
+ "crypto/sha256"
"crypto/sha512"
"encoding/binary"
"encoding/json"
@@ -200,14 +201,7 @@ func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Cannot read body", http.StatusBadRequest)
return
}
- tx := m.Db.Where("h_mailbox = ?", vars["h_mailbox"])
- // FIXME max messages from config
- // FIXME unclear if this is how the API is defined
- if tx.RowsAffected > 10 {
- w.WriteHeader(http.StatusTooManyRequests)
- return
- }
- err = m.Db.First(&entry, "h_mailbox = ? AND body = ?", vars["h_mailbox"], body).Error
+ err = m.Db.First(&entry, "h_mailbox = ? AND body = ?", vars["h_mailbox"], body, true).Error
if err != nil {
entry.HMailbox = vars["h_mailbox"]
entry.Body = body
@@ -215,7 +209,14 @@ func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
if !m.MessageFee.IsZero() {
if len(entry.OrderID) == 0 {
// Add new order for new entry
- orderID, newOrderErr := m.Merchant.AddNewOrder(*m.MessageFee, "Mailbox message dispatch", m.BaseUrl)
+ var order merchant.CommonOrder
+ order.Amount = m.MessageFee.String()
+ order.Summary = "Mailbox message dispatch"
+ order.MerchantBaseUrl = m.BaseUrl
+ h := sha256.New()
+ h.Sum(body)
+ order.OrderId = gnunetutil.EncodeBinaryToString(h.Sum(nil))
+ orderID, newOrderErr := m.Merchant.CreateOrder(order)
if newOrderErr != nil {
fmt.Println(newOrderErr)
w.WriteHeader(http.StatusInternalServerError)
@@ -224,168 +225,170 @@ func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
entry.OrderID = orderID
}
// Check if order paid.
- _, _, payto, paytoErr := m.Merchant.IsOrderPaid(entry.OrderID)
+ _, paymentStatus, payto, paytoErr := m.Merchant.IsOrderPaid(entry.OrderID)
if paytoErr != nil {
fmt.Println(paytoErr)
w.WriteHeader(http.StatusInternalServerError)
log.Println(paytoErr)
return
}
- if len(payto) != 0 {
- m.Db.Save(&entry)
- w.WriteHeader(http.StatusPaymentRequired)
- w.Header().Set("Taler", payto)
- return
- }
- }
- // In this case, this order was paid
- m.Db.Save(&entry)
- w.WriteHeader(http.StatusNoContent)
+ if paymentStatus != "paid" {
+ m.Db.Save(&entry)
+ w.WriteHeader(http.StatusPaymentRequired)
+ if payto != "" {
+ w.Header().Set("Taler", payto)
+ }
+ return
+ }
+ }
+ // In this case, this order was paid
+ m.Db.Save(&entry)
+ w.WriteHeader(http.StatusNoContent)
}
func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- var msg MessageDeletionRequest
- var entries []InboxEntry
- if r.Body == nil {
- http.Error(w, "No request body", 400)
- return
- }
- err := json.NewDecoder(r.Body).Decode(&msg)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- etag_hdr := r.Header.Get("If-Match")
- if etag_hdr == "" {
- http.Error(w, "If-Match header missing", 400)
- return
- }
- if strings.Contains(etag_hdr, ",") {
- http.Error(w, "If-Match contains multiple values", 400)
- return
- }
- etag_expected, err := strconv.Atoi(etag_hdr)
- if err != nil {
- http.Error(w, "If-Match contains malformed etag number", 400)
- return
- }
- pkey, err := gnunetutil.DecodeStringToBinary(vars["mailbox"], 32)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- checksum, err := gnunetutil.DecodeStringToBinary(msg.Checksum, 64)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- pk := ed25519.PublicKey(pkey)
- sig, err := gnunetutil.DecodeStringToBinary(msg.WalletSig, 64)
- if nil != err {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- var signed_msg [72]byte
+ vars := mux.Vars(r)
+ var msg MessageDeletionRequest
+ var entries []InboxEntry
+ if r.Body == nil {
+ http.Error(w, "No request body", 400)
+ return
+ }
+ err := json.NewDecoder(r.Body).Decode(&msg)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ etag_hdr := r.Header.Get("If-Match")
+ if etag_hdr == "" {
+ http.Error(w, "If-Match header missing", 400)
+ return
+ }
+ if strings.Contains(etag_hdr, ",") {
+ http.Error(w, "If-Match contains multiple values", 400)
+ return
+ }
+ etag_expected, err := strconv.Atoi(etag_hdr)
+ if err != nil {
+ http.Error(w, "If-Match contains malformed etag number", 400)
+ return
+ }
+ pkey, err := gnunetutil.DecodeStringToBinary(vars["mailbox"], 32)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ checksum, err := gnunetutil.DecodeStringToBinary(msg.Checksum, 64)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ pk := ed25519.PublicKey(pkey)
+ sig, err := gnunetutil.DecodeStringToBinary(msg.WalletSig, 64)
+ if nil != err {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ var signed_msg [72]byte
size := signed_msg[0:4]
- binary.BigEndian.PutUint32(size, 64+4+4)
+ binary.BigEndian.PutUint32(size, 64+4+4)
purp := signed_msg[4:8]
- binary.BigEndian.PutUint32(purp, gana.TALER_SIGNATURE_PURPOSE_MAILBOX_MESSAGES_DELETE)
+ binary.BigEndian.PutUint32(purp, gana.TALER_SIGNATURE_PURPOSE_MAILBOX_MESSAGES_DELETE)
copy(signed_msg[8:], checksum)
if !ed25519.Verify(pk, signed_msg[0:], sig) {
- w.WriteHeader(http.StatusForbidden)
- return
- }
- h := sha512.New()
- h.Write(pkey)
- h_mailbox := gnunetutil.EncodeBinaryToString(h.Sum(nil))
- err = m.Db.Where("h_mailbox = ? AND id >= ?", h_mailbox, etag_expected).Limit(msg.Count).Find(&entries).Error
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- if len(entries) != msg.Count {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- if entries[0].ID != uint(etag_expected) {
- w.WriteHeader(http.StatusPreconditionFailed)
- return
- }
- h_all := sha512.New()
- for _, entry := range entries {
- h_all.Write(entry.Body)
- }
- if !bytes.Equal(h_all.Sum(nil), checksum) {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- m.Db.Delete(&entries)
- w.WriteHeader(http.StatusNoContent)
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+ h := sha512.New()
+ h.Write(pkey)
+ h_mailbox := gnunetutil.EncodeBinaryToString(h.Sum(nil))
+ err = m.Db.Where("h_mailbox = ? AND id >= ?", h_mailbox, etag_expected).Limit(msg.Count).Find(&entries).Error
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ if len(entries) != msg.Count {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ if entries[0].ID != uint(etag_expected) {
+ w.WriteHeader(http.StatusPreconditionFailed)
+ return
+ }
+ h_all := sha512.New()
+ for _, entry := range entries {
+ h_all.Write(entry.Body)
+ }
+ if !bytes.Equal(h_all.Sum(nil), checksum) {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ m.Db.Delete(&entries)
+ w.WriteHeader(http.StatusNoContent)
}
func (m *Mailbox) termsResponse(w http.ResponseWriter, r *http.Request) {
- tos.ServiceTermsResponse(m.Cfg.Section("mailbox"), w, r)
+ tos.ServiceTermsResponse(m.Cfg.Section("mailbox"), w, r)
}
func (m *Mailbox) privacyResponse(w http.ResponseWriter, r *http.Request) {
- tos.PrivacyPolicyResponse(m.Cfg.Section("mailbox"), w, r)
+ tos.PrivacyPolicyResponse(m.Cfg.Section("mailbox"), w, r)
}
func (m *Mailbox) setupHandlers() {
- m.Router = mux.NewRouter().StrictSlash(true)
+ m.Router = mux.NewRouter().StrictSlash(true)
- /* ToS API */
- m.Router.HandleFunc("/terms", m.termsResponse).Methods("GET")
- m.Router.HandleFunc("/privacy", m.privacyResponse).Methods("GET")
+ /* ToS API */
+ m.Router.HandleFunc("/terms", m.termsResponse).Methods("GET")
+ m.Router.HandleFunc("/privacy", m.privacyResponse).Methods("GET")
- /* Config API */
- m.Router.HandleFunc("/config", m.configResponse).Methods("GET")
+ /* Config API */
+ m.Router.HandleFunc("/config", m.configResponse).Methods("GET")
- /* Mailbox API */
- m.Router.HandleFunc("/{h_mailbox}", m.sendMessageResponse).Methods("POST")
- m.Router.HandleFunc("/{h_mailbox}", m.getMessagesResponse).Methods("GET")
- m.Router.HandleFunc("/{mailbox}", m.deleteMessagesResponse).Methods("DELETE")
+ /* Mailbox API */
+ m.Router.HandleFunc("/{h_mailbox}", m.sendMessageResponse).Methods("POST")
+ m.Router.HandleFunc("/{h_mailbox}", m.getMessagesResponse).Methods("GET")
+ m.Router.HandleFunc("/{mailbox}", m.deleteMessagesResponse).Methods("DELETE")
}
// Initialize the Mailbox instance with cfgfile
func (m *Mailbox) Initialize(cfgfile string) {
- _cfg, err := ini.Load(cfgfile)
- if err != nil {
- fmt.Printf("Failed to read config: %v", err)
- os.Exit(1)
- }
- m.Cfg = _cfg
- if m.Cfg.Section("mailbox").Key("production").MustBool(false) {
- fmt.Println("Production mode enabled")
- }
- m.BaseUrl = m.Cfg.Section("mailbox").Key("base_url").MustString("https://example.com")
- m.MessageBodyBytes = m.Cfg.Section("mailbox").Key("message_body_bytes").MustInt64(256)
- // FIXME actual cost
- m.MessageFee, err = talerutil.ParseAmount(m.Cfg.Section("mailbox").Key("message_fee").MustString("KUDOS:1"))
- if err != nil {
- fmt.Printf("Failed to parse cost: %v", err)
- os.Exit(1)
- }
- psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
- m.Cfg.Section("mailbox-pq").Key("host").MustString("localhost"),
- m.Cfg.Section("mailbox-pq").Key("port").MustInt64(5432),
- m.Cfg.Section("mailbox-pq").Key("user").MustString("taler-mailbox"),
- m.Cfg.Section("mailbox-pq").Key("password").MustString("secret"),
- m.Cfg.Section("mailbox-pq").Key("db_name").MustString("taler-mailbox"))
- _db, err := gorm.Open(postgres.Open(psqlconn), &gorm.Config{
- Logger: logger.Default.LogMode(logger.Silent),
- })
- if err != nil {
- panic(err)
- }
- m.Db = _db
- if err := m.Db.AutoMigrate(&InboxEntry{}); err != nil {
- panic(err)
- }
-
- merchURL := m.Cfg.Section("mailbox").Key("merchant_baseurl_private").MustString("http://merchant.mailbox/instances/myInstance")
- merchToken := m.Cfg.Section("mailbox").Key("merchant_token").MustString("secretAccessToken")
- m.Merchant = merchant.NewMerchant(merchURL, merchToken)
- m.setupHandlers()
+ _cfg, err := ini.Load(cfgfile)
+ if err != nil {
+ fmt.Printf("Failed to read config: %v", err)
+ os.Exit(1)
+ }
+ m.Cfg = _cfg
+ if m.Cfg.Section("mailbox").Key("production").MustBool(false) {
+ fmt.Println("Production mode enabled")
+ }
+ m.BaseUrl = m.Cfg.Section("mailbox").Key("base_url").MustString("https://example.com")
+ m.MessageBodyBytes = m.Cfg.Section("mailbox").Key("message_body_bytes").MustInt64(256)
+ // FIXME actual cost
+ m.MessageFee, err = talerutil.ParseAmount(m.Cfg.Section("mailbox").Key("message_fee").MustString("KUDOS:1"))
+ if err != nil {
+ fmt.Printf("Failed to parse cost: %v", err)
+ os.Exit(1)
+ }
+ psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
+ m.Cfg.Section("mailbox-pq").Key("host").MustString("localhost"),
+ m.Cfg.Section("mailbox-pq").Key("port").MustInt64(5432),
+ m.Cfg.Section("mailbox-pq").Key("user").MustString("taler-mailbox"),
+ m.Cfg.Section("mailbox-pq").Key("password").MustString("secret"),
+ m.Cfg.Section("mailbox-pq").Key("db_name").MustString("taler-mailbox"))
+ _db, err := gorm.Open(postgres.Open(psqlconn), &gorm.Config{
+ Logger: logger.Default.LogMode(logger.Silent),
+ })
+ if err != nil {
+ panic(err)
+ }
+ m.Db = _db
+ if err := m.Db.AutoMigrate(&InboxEntry{}); err != nil {
+ panic(err)
+ }
+
+ merchURL := m.Cfg.Section("mailbox").Key("merchant_baseurl_private").MustString("http://merchant.mailbox/instances/myInstance")
+ merchToken := m.Cfg.Section("mailbox").Key("merchant_token").MustString("secretAccessToken")
+ m.Merchant = merchant.NewMerchant(merchURL, merchToken)
+ m.setupHandlers()
}