Skip to content
This repository has been archived by the owner on Sep 12, 2019. It is now read-only.

Commit

Permalink
Merge pull request #71 from poliha/tx_status
Browse files Browse the repository at this point in the history
Tx status
  • Loading branch information
bartekn authored Sep 8, 2017
2 parents 43e4675 + 4540431 commit 74e1500
Show file tree
Hide file tree
Showing 13 changed files with 782 additions and 5 deletions.
5 changes: 5 additions & 0 deletions compliance_example.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ encryption_key = ""
sanctions = "http://sanctions"
ask_user = "http://ask_user"
fetch_info = "http://fetch_info"
tx_status = "http://tx_status"

[tls]
certificate_file = "server.crt"
private_key_file = "server.key"

[tx_status_auth]
username = "username"
password = "password"
27 changes: 26 additions & 1 deletion readme_compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ The `compliance.cfg` file must be present in a working directory (you can load a
* `sanctions` - Callback that performs sanctions check. Read [Callbacks](#callbacks) section.
* `ask_user` - Callback that asks user for permission for reading their data. Read [Callbacks](#callbacks) section.
* `fetch_info` - Callback that returns user data. Read [Callbacks](#callbacks) section.
* `tx_status` - Callback that returns user data. Read [Callbacks](#callbacks) section.
* `tls` (only when running HTTPS external server)
* `certificate_file` - a file containing a certificate
* `private_key_file` - a file containing a matching private key
* `log_format` - set to `json` for JSON logs
* `tx_status_auth` - authentication credentials for `/tx_status` endpoint.
* `username`
* `password` - minimum 10 chars

Check [`compliance_example.cfg`](./compliance_example.cfg).

Expand Down Expand Up @@ -76,7 +80,7 @@ Returns [Auth response](https://www.stellar.org/developers/learn/integration-gui

### POST :internal_port/send

Typically called by the bridge server when a user initiates a payment. This endpoint causes the compliance server to send an Auth request to another organization. It will call the Auth endpoint of the receiving instition.
Typically called by the bridge server when a user initiates a payment. This endpoint causes the compliance server to send an Auth request to another organization. It will call the Auth endpoint of the receiving instition.

#### Request Parameters

Expand Down Expand Up @@ -239,6 +243,27 @@ This callback should return `200 OK` status code and JSON object with the custom
"date_of_birth": "1990-01-01"
}
```
### `callbacks.tx_status`
This callback should return the status of a transaction as explained in [`SEP-0001`](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md).

#### Request

name | description
--- | ---
`id` | Stellar transaction ID.

#### Response
This callback should return `200 OK` status code and JSON object with the transaction status info:

```json
{
"status": "status code as defined in SEP-0001",
"recv_code": "arbitrary string",
"refund_tx": "tx_hash",
"msg": "arbitrary string"
}
```


Any other status code will be considered an error.

Expand Down
5 changes: 4 additions & 1 deletion src/github.com/stellar/gateway/compliance/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package compliance

import (
"fmt"
log "github.com/Sirupsen/logrus"
"net/http"
"os"
"time"

log "github.com/Sirupsen/logrus"
"github.com/goji/httpauth"

"github.com/facebookgo/inject"
"github.com/stellar/gateway/compliance/config"
"github.com/stellar/gateway/compliance/handlers"
Expand Down Expand Up @@ -110,6 +112,7 @@ func (a *App) Serve() {
external.Use(server.StripTrailingSlashMiddleware())
external.Use(server.HeadersMiddleware())
external.Post("/", a.requestHandler.HandlerAuth)
external.Get("/tx_status", httpauth.SimpleBasicAuth(a.config.TxStatusAuth.Username, a.config.TxStatusAuth.Password)(http.HandlerFunc(a.requestHandler.HandlerTxStatus)))
externalPortString := fmt.Sprintf(":%d", *a.config.ExternalPort)
log.Println("Starting external server on", externalPortString)
go func() {
Expand Down
11 changes: 8 additions & 3 deletions src/github.com/stellar/gateway/compliance/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type Config struct {
CertificateFile string `mapstructure:"certificate_file"`
PrivateKeyFile string `mapstructure:"private_key_file"`
}
TxStatusAuth struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
} `mapstructure:"tx_status_auth"`
}

// Keys contains values of `keys` config group
Expand All @@ -36,6 +40,7 @@ type Callbacks struct {
Sanctions string
AskUser string `mapstructure:"ask_user"`
FetchInfo string `mapstructure:"fetch_info"`
TxStatus string `mapstructure:"tx_status"`
}

// Validate validates config and returns error if any of config values is incorrect
Expand Down Expand Up @@ -97,10 +102,10 @@ func (c *Config) Validate() (err error) {
}
}

if c.Callbacks.Sanctions != "" {
_, err = url.Parse(c.Callbacks.Sanctions)
if c.Callbacks.TxStatus != "" {
_, err = url.Parse(c.Callbacks.TxStatus)
if err != nil {
err = errors.New("Cannot parse callbacks.sanctions param")
err = errors.New("Cannot parse callbacks.tx_status param")
return
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package handlers

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"

log "github.com/Sirupsen/logrus"
"github.com/stellar/gateway/protocols"
"github.com/stellar/gateway/server"
"github.com/stellar/go/protocols/compliance"
)

// HandlerTxStatus implements /tx_status endpoint
func (rh *RequestHandler) HandlerTxStatus(w http.ResponseWriter, r *http.Request) {

txid := r.URL.Query().Get("id")
if txid == "" {
log.Info("unable to get query parameter")
server.Write(w, protocols.NewMissingParameter("id"))
return
}
response := compliance.TransactionStatusResponse{}

if rh.Config.Callbacks.TxStatus == "" {
response.Status = compliance.TransactionStatusUnknown
} else {

u, err := url.Parse(rh.Config.Callbacks.TxStatus)
if err != nil {
log.Error(err, "failed to parse tx status endpoint")
server.Write(w, protocols.InternalServerError)
return
}

q := u.Query()
q.Set("id", txid)
u.RawQuery = q.Encode()
resp, err := rh.Client.Get(u.String())
if err != nil {
log.WithFields(log.Fields{
"tx_status": u.String(),
"err": err,
}).Error("Error sending request to tx_status server")
server.Write(w, protocols.InternalServerError)
return
}

defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("Error reading tx_status server response")
server.Write(w, protocols.InternalServerError)
return
}

switch resp.StatusCode {
case http.StatusOK:
err := json.Unmarshal(body, &response)
if err != nil {
log.WithFields(log.Fields{
"tx_status": rh.Config.Callbacks.TxStatus,
"body": string(body),
}).Error("Unable to decode tx_status response")
server.Write(w, protocols.InternalServerError)
return
}
if response.Status == "" {
response.Status = compliance.TransactionStatusUnknown
}

default:
response.Status = compliance.TransactionStatusUnknown
}
}

w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(response)
if err != nil {
log.Error("Error encoding tx status response")
server.Write(w, protocols.InternalServerError)
return
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package handlers

import (
"net/http"
"testing"

"github.com/facebookgo/inject"
"github.com/goji/httpauth"
. "github.com/smartystreets/goconvey/convey"
"github.com/stellar/gateway/compliance/config"
"github.com/stellar/gateway/mocks"
"github.com/stellar/gateway/net"
"github.com/stellar/go/support/http/httptest"
)

func TestRequestHandlerTxStatus(t *testing.T) {
c := &config.Config{
NetworkPassphrase: "Test SDF Network ; September 2015",
Keys: config.Keys{
// GBYJZW5XFAI6XV73H5SAIUYK6XZI4CGGVBUBO3ANA2SV7KKDAXTV6AEB
SigningSeed: "SDWTLFPALQSP225BSMX7HPZ7ZEAYSUYNDLJ5QI3YGVBNRUIIELWH3XUV",
},
}
c.TxStatusAuth.Username = "username"
c.TxStatusAuth.Password = "password"

txid := "abc123"
mockHTTPClient := new(mocks.MockHTTPClient)
mockEntityManager := new(mocks.MockEntityManager)
mockRepository := new(mocks.MockRepository)
mockFederationResolver := new(mocks.MockFederationResolver)
mockSignerVerifier := new(mocks.MockSignerVerifier)
mockStellartomlResolver := new(mocks.MockStellartomlResolver)
requestHandler := RequestHandler{}

// Inject mocks
var g inject.Graph

err := g.Provide(
&inject.Object{Value: &requestHandler},
&inject.Object{Value: c},
&inject.Object{Value: mockHTTPClient},
&inject.Object{Value: mockEntityManager},
&inject.Object{Value: mockRepository},
&inject.Object{Value: mockFederationResolver},
&inject.Object{Value: mockSignerVerifier},
&inject.Object{Value: mockStellartomlResolver},
&inject.Object{Value: &TestNonceGenerator{}},
)
if err != nil {
panic(err)
}

if err := g.Populate(); err != nil {
panic(err)
}

httpHandle := func(w http.ResponseWriter, r *http.Request) {
requestHandler.HandlerTxStatus(w, r)
}

testServer := httptest.NewServer(t, httpauth.SimpleBasicAuth(c.TxStatusAuth.Username,
c.TxStatusAuth.Password)(http.HandlerFunc(httpHandle)))
defer testServer.Close()

Convey("Given tx_status request", t, func() {
Convey("it returns unauthorised when no auth", func() {
testServer.GET("/tx_status").
WithQuery("id", "123").
Expect().
Status(http.StatusUnauthorized)
})
Convey("it returns unauthorised when bad auth", func() {
testServer.GET("/tx_status").
WithBasicAuth("username", "wrong_password").
Expect().
Status(http.StatusUnauthorized)
})
Convey("it returns bad request when no parameter", func() {
testServer.GET("/tx_status").
WithBasicAuth("username", "password").
Expect().
Status(http.StatusBadRequest)
})
Convey("it returns unknown when no tx_status endpoint", func() {
testServer.GET("/tx_status").
WithBasicAuth("username", "password").
WithQuery("id", "123").
Expect().
Status(http.StatusOK).
Body().Equal(`{"status":"unknown"}` + "\n")
})
Convey("it returns unknown when valid endpoint returns bad request", func() {
c.Callbacks = config.Callbacks{
TxStatus: "http://tx_status",
}

mockHTTPClient.On(
"Get",
"http://tx_status?id="+txid,
).Return(
net.BuildHTTPResponse(400, "badrequest"),
nil,
).Once()

testServer.GET("/tx_status").
WithBasicAuth("username", "password").
WithQuery("id", txid).
Expect().
Status(http.StatusOK).
Body().Equal(`{"status":"unknown"}` + "\n")
})

Convey("it returns unknown when valid endpoint returns empty data", func() {
c.Callbacks = config.Callbacks{
TxStatus: "http://tx_status",
}

mockHTTPClient.On(
"Get",
"http://tx_status?id="+txid,
).Return(
net.BuildHTTPResponse(200, "{}"),
nil,
).Once()

testServer.GET("/tx_status").
WithBasicAuth("username", "password").
WithQuery("id", txid).
Expect().
Status(http.StatusOK).
Body().Equal(`{"status":"unknown"}` + "\n")
})

Convey("it returns response from valid endpoint with data", func() {
c.Callbacks = config.Callbacks{
TxStatus: "http://tx_status",
}

mockHTTPClient.On(
"Get",
"http://tx_status?id="+txid,
).Return(
net.BuildHTTPResponse(200, `{"status":"delivered","msg":"cash paid"}`),
nil,
).Once()

testServer.GET("/tx_status").
WithBasicAuth("username", "password").
WithQuery("id", txid).
Expect().
Status(http.StatusOK).
Body().Equal(`{"status":"delivered","msg":"cash paid"}` + "\n")
})

})
}
6 changes: 6 additions & 0 deletions src/github.com/stellar/gateway/mocks/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ func (m *MockHTTPClient) PostForm(url string, data url.Values) (resp *http.Respo
return a.Get(0).(*http.Response), a.Error(1)
}

// Get is a mocking a method
func (m *MockHTTPClient) Get(url string) (resp *http.Response, err error) {
a := m.Called(url)
return a.Get(0).(*http.Response), a.Error(1)
}

// Do is a mocking a method
func (m *MockHTTPClient) Do(req *http.Request) (resp *http.Response, err error) {
a := m.Called(req)
Expand Down
1 change: 1 addition & 0 deletions src/github.com/stellar/gateway/net/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
// HTTPClientInterface helps mocking http.Client in tests
type HTTPClientInterface interface {
PostForm(url string, data url.Values) (resp *http.Response, err error)
Get(url string) (resp *http.Response, err error)
}

// BuildHTTPResponse is used in tests
Expand Down
6 changes: 6 additions & 0 deletions vendor/manifest
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@
"revision": "c9c5955059ba2e3d6a25e8f1a555e59015d4abba",
"branch": "master"
},
{
"importpath": "github.com/goji/httpauth",
"repository": "https://github.com/goji/httpauth",
"revision": "2da839ab0f4df05a6db5eb277995589dadbd4fb9",
"branch": "master"
},
{
"importpath": "github.com/goji/param",
"repository": "https://github.com/goji/param",
Expand Down
Loading

0 comments on commit 74e1500

Please sign in to comment.