")
-}
-
-//data changes requested by customer
-func changeDataBeforeSend(dat interface{}) interface{} {
- if m, ok := dat.(map[string]interface{}); ok {
- if d, ok := m["input"]; ok {
- bts, _ := json.Marshal(d)
- var dataCopy map[string]interface{}
- json.Unmarshal(bts, &dataCopy)
- if cs, ok := dataCopy["CapitalSource"]; ok {
- bts, _ := json.Marshal(cs)
- dataCopy["CapitalSource"] = string(bts)
- }
- return dataCopy
- }
- }
- return dat
-}
-
-func (me *IBMSenderNodeImpl) Execute(n *workflow.Node) (proceed bool, err error) {
- b, err := json.Marshal(changeDataBeforeSend(me.ctx.getData()))
- if err != nil {
- return false, err
- }
- req, err := http.NewRequest("POST", forwardURL, bytes.NewReader(b))
- if err != nil {
- return false, err
- }
- req.Header.Set("Content-Type", "application/json")
- addConfigHeaders(req)
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- b2, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 100*1024))
- log.Printf("SERVER NOT ACCEPTED '%s', RESPONSE '%s'\n", b, b2)
- return false, err
- }
- return true, nil
-}
-
-func (me *IBMSenderNodeImpl) Remove(n *workflow.Node) {}
-func (me *IBMSenderNodeImpl) Close() {}
diff --git a/main/app/ibm_sender_test.go b/main/app/ibm_sender_test.go
deleted file mode 100644
index cc9561ef6..000000000
--- a/main/app/ibm_sender_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package app
-
-import (
- "encoding/json"
- "testing"
-)
-
-func TestChangeDataBeforeSend(t *testing.T) {
- dat := map[string]interface{}{"input": map[string]interface{}{"CapitalSource": []interface{}{"Andere"}}}
- newDat := changeDataBeforeSend(dat)
- bts, _ := json.Marshal(newDat)
- if string(bts) != `{"CapitalSource":"[\"Andere\"]"}` {
- t.Error(string(bts))
- }
-}
diff --git a/main/app/ibm_sender_test.go-e b/main/app/ibm_sender_test.go-e
deleted file mode 100644
index cc9561ef6..000000000
--- a/main/app/ibm_sender_test.go-e
+++ /dev/null
@@ -1,15 +0,0 @@
-package app
-
-import (
- "encoding/json"
- "testing"
-)
-
-func TestChangeDataBeforeSend(t *testing.T) {
- dat := map[string]interface{}{"input": map[string]interface{}{"CapitalSource": []interface{}{"Andere"}}}
- newDat := changeDataBeforeSend(dat)
- bts, _ := json.Marshal(newDat)
- if string(bts) != `{"CapitalSource":"[\"Andere\"]"}` {
- t.Error(string(bts))
- }
-}
diff --git a/main/config/configuration.go b/main/config/configuration.go
index 73e71f282..b991aa7c8 100644
--- a/main/config/configuration.go
+++ b/main/config/configuration.go
@@ -23,6 +23,9 @@ type Configuration struct {
XESContractAddress string `json:"XESContractAddress" default:"0x84E0b37e8f5B4B86d5d299b0B0e33686405A3919"`
+ AirdropWalletfile string `json:"airdropWalletfile" usage:"Path to File containing Private Key of the Wallet to fund Airdrops of XES and Ether."`
+ AirdropWalletkey string `json:"airdropWalletkey" usage:"Path to File containing the Key for the Airdrop Private Key."`
+
model.Settings // extend cmd line args with settings
}
diff --git a/main/config/configuration.go-e b/main/config/configuration.go-e
index 73e71f282..b991aa7c8 100644
--- a/main/config/configuration.go-e
+++ b/main/config/configuration.go-e
@@ -23,6 +23,9 @@ type Configuration struct {
XESContractAddress string `json:"XESContractAddress" default:"0x84E0b37e8f5B4B86d5d299b0B0e33686405A3919"`
+ AirdropWalletfile string `json:"airdropWalletfile" usage:"Path to File containing Private Key of the Wallet to fund Airdrops of XES and Ether."`
+ AirdropWalletkey string `json:"airdropWalletkey" usage:"Path to File containing the Key for the Airdrop Private Key."`
+
model.Settings // extend cmd line args with settings
}
diff --git a/main/customNode/repository.go b/main/customNode/repository.go
index 4aedf529e..067aee2c6 100644
--- a/main/customNode/repository.go
+++ b/main/customNode/repository.go
@@ -6,12 +6,6 @@ import (
func List(nodeType string) *workflow.Node {
var repositories = make(map[string]*workflow.Node)
- repositories["ibmsender"] = &workflow.Node{
- ID: "1234123-1234123",
- Name: "IBM Sender",
- Detail: "sends all workflow data to an IBM service",
- Type: "ibmsender",
- }
repositories["mailsender"] = &workflow.Node{
ID: "1234123-1234124",
diff --git a/main/customNode/repository.go-e b/main/customNode/repository.go-e
index 4aedf529e..067aee2c6 100644
--- a/main/customNode/repository.go-e
+++ b/main/customNode/repository.go-e
@@ -6,12 +6,6 @@ import (
func List(nodeType string) *workflow.Node {
var repositories = make(map[string]*workflow.Node)
- repositories["ibmsender"] = &workflow.Node{
- ID: "1234123-1234123",
- Name: "IBM Sender",
- Detail: "sends all workflow data to an IBM service",
- Type: "ibmsender",
- }
repositories["mailsender"] = &workflow.Node{
ID: "1234123-1234124",
diff --git a/main/handlers/api/handlers.go b/main/handlers/api/handlers.go
index 14bb46033..6deb952d8 100644
--- a/main/handlers/api/handlers.go
+++ b/main/handlers/api/handlers.go
@@ -12,6 +12,10 @@ import (
"strings"
"time"
+ workflow2 "git.proxeus.com/core/central/sys/workflow"
+
+ "git.proxeus.com/core/central/main/handlers/payment"
+
"git.proxeus.com/core/central/main/handlers/blockchain"
"git.proxeus.com/core/central/sys/utils"
@@ -48,7 +52,6 @@ import (
)
var filenameRegex = regexp.MustCompile(`^[^\s][\p{L}\d.,_\-&: ]{3,}[^\s]$`)
-var ServerVersion string
func html(c echo.Context, p string) error {
bts, err := sys.ReadAllFile(p)
@@ -249,7 +252,6 @@ func GetInit(e echo.Context) error {
if len(settings.PlatformDomain) == 0 {
settings.PlatformDomain = e.Request().Host
}
-
return c.JSON(http.StatusOK, map[string]interface{}{"settings": settings, "configured": configured})
}
@@ -305,20 +307,22 @@ func PostInit(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
-func ConfigHandler(e echo.Context) error {
- c := e.(*www.Context)
- sess := c.Session(false)
- var roles []model.RoleSet
- if sess != nil {
- roles = sess.AccessRights().RolesInRange()
+func ConfigHandler(version string) echo.HandlerFunc {
+ return func(e echo.Context) error {
+ c := e.(*www.Context)
+ sess := c.Session(false)
+ var roles []model.RoleSet
+ if sess != nil {
+ roles = sess.AccessRights().RolesInRange()
+ }
+ stngs := c.System().GetSettings()
+ return c.JSON(http.StatusOK, map[string]interface{}{
+ "roles": roles,
+ "blockchainNet": strings.Replace(stngs.BlockchainNet, "mainnet", "main", 1),
+ "blockchainProxeusFSAddress": stngs.BlockchainContractAddress,
+ "version": version,
+ })
}
- stngs := c.System().GetSettings()
- return c.JSON(http.StatusOK, map[string]interface{}{
- "roles": roles,
- "blockchainNet": strings.Replace(stngs.BlockchainNet, "mainnet", "main", 1),
- "blockchainProxeusFSAddress": stngs.BlockchainContractAddress,
- "version": ServerVersion,
- })
}
type loginForm struct {
@@ -468,10 +472,62 @@ func LoginWithWallet(c *www.Context, challenge, signature string) (bool, *model.
}
created = true
usr, err = c.System().DB.User.GetByBCAddress(address)
+ if err == nil && c.System().GetSettings().BlockchainNet == "ropsten" && c.System().GetSettings().AirdropEnabled == "true" {
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Println("airdrop recover with err ", r)
+ }
+ }()
+ blockchain.GiveTokens(address)
+ }()
+ }
}
return created, usr, err
}
+func GetSessionTokenHandler(e echo.Context) (err error) {
+ c := e.(*www.Context)
+
+ username, apiKey := c.BasicAuth()
+
+ if username == "" || apiKey == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ user, err := c.System().DB.User.APIKey(apiKey)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if user == nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if user.Email != username && user.EthereumAddr != username {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ //create a new session only if role, id or name has changed
+ sess := c.SessionWithUser(user)
+ if sess == nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ sess.Put("user", user)
+
+ c.Response().Header().Del("Set-Cookie")
+
+ return c.JSON(http.StatusOK, map[string]string{
+ "token": sess.ID(),
+ })
+}
+
+func DeleteSessionTokenHandler(e echo.Context) (err error) {
+ c := e.(*www.Context)
+ c.EndSession()
+ return c.NoContent(http.StatusOK)
+}
+
type TokenRequest struct {
Email string `json:"email" validate:"email=true,required=true"`
Token string `json:"token"`
@@ -545,38 +601,50 @@ func RegisterRequest(e echo.Context) (err error) {
stngs := c.System().GetSettings()
m.Role = stngs.DefaultRole
- if usr, err := c.System().DB.User.GetByEmail(m.Email); usr == nil {
- resetKey := m.Email + "_register"
- var token *TokenRequest
- err = c.System().Cache.Get(resetKey, &token)
+ if usr, _ := c.System().DB.User.GetByEmail(m.Email); usr != nil {
+ // always return ok if provided email was valid
+ // otherwise public users can test what email accounts exist
+ return c.NoContent(http.StatusOK)
+ }
+
+ resetKey := m.Email + "_register"
+ var token *TokenRequest
+
+ err = c.System().Cache.Get(resetKey, &token)
+ if err == nil {
+ return c.NoContent(http.StatusOK)
+ }
+
+ token = m
+ u2 := uuid.NewV4()
+ token.Token = u2.String()
+
+ if c.System().TestMode {
+ c.Response().Header().Set("X-Test-Token", token.Token)
+ } else {
+ err = c.System().EmailSender.Send(&email.Email{
+ From: stngs.EmailFrom,
+ To: []string{m.Email},
+ Subject: c.I18n().T("Register"),
+ Body: fmt.Sprintf(
+ "Hi there,\n\nplease proceed with your registration by visiting this link:\n%s\n\nIf you didn't request this, please ignore this email.\n\nProxeus",
+ helpers.AbsoluteURL(c, "/register/", token.Token),
+ ),
+ })
if err != nil {
- token = m
- u2 := uuid.NewV4()
- token.Token = u2.String()
- err = c.System().EmailSender.Send(&email.Email{
- From: stngs.EmailFrom,
- To: []string{m.Email},
- Subject: c.I18n().T("Register"),
- Body: fmt.Sprintf(
- "Hi there,\n\nplease proceed with your registration by visiting this link:\n%s\n\nIf you didn't request this, please ignore this email.\n\nProxeus",
- helpers.AbsoluteURL(c, "/register/", token.Token),
- ),
- })
- if err != nil {
- return c.NoContent(http.StatusExpectationFailed)
- }
- err = c.System().Cache.Put(resetKey, token)
- if err != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
- err = c.System().Cache.Put(token.Token, token)
- if err != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
+ return c.NoContent(http.StatusExpectationFailed)
}
}
- // always return ok if provided email was valid
- // otherwise public users can test what email accounts exist
+
+ err = c.System().Cache.Put(resetKey, token)
+ if err != nil {
+ return c.NoContent(http.StatusInternalServerError)
+ }
+ err = c.System().Cache.Put(token.Token, token)
+ if err != nil {
+ return c.NoContent(http.StatusInternalServerError)
+ }
+
return c.NoContent(http.StatusOK)
}
@@ -602,6 +670,71 @@ func Register(e echo.Context) error {
if err != nil {
return c.NoContent(http.StatusExpectationFailed)
}
+
+ // If some default workflows have to be assigned to the user, then clone them
+ workflowIds := strings.Split(c.System().GetSettings().DefaultWorkflowIds, ",")
+ workflows, err := c.System().DB.Workflow.GetList(root, workflowIds)
+ if err != nil {
+ log.Printf("Can't retrieve list of workflows (%v). Please check the ids exist. Error: %s", workflowIds, err.Error())
+ }
+ for _, workflow := range workflows {
+ w := workflow.Clone()
+ w.OwnerEthAddress = newUser.EthereumAddr
+ w.Owner = newUser.ID
+ newNodes := make(map[string]*workflow2.Node)
+ oldToNewIdsMap := make(map[string]string)
+ for oldId, node := range w.Data.Flow.Nodes {
+ if node.Type == "form" {
+ form, er := c.System().DB.Form.Get(root, node.ID)
+ if er != nil {
+ log.Println(err.Error())
+ }
+ f := form.Clone()
+ er = c.System().DB.Form.Put(newUser, &f)
+ if er != nil {
+ log.Println("can't put form" + err.Error())
+ }
+
+ oldToNewIdsMap[node.ID] = f.ID
+ node.ID = f.ID
+ newNodes[node.ID] = node
+ delete(w.Data.Flow.Nodes, oldId)
+
+ } else if node.Type == "template" {
+ template, er := c.System().DB.Template.Get(root, node.ID)
+ if er != nil {
+ log.Println(err.Error())
+ }
+ t := template.Clone()
+ er = c.System().DB.Template.Put(newUser, &t)
+ if er != nil {
+ log.Println("can't put template" + err.Error())
+ }
+ oldToNewIdsMap[node.ID] = t.ID
+ node.ID = t.ID
+ newNodes[node.ID] = node
+ delete(w.Data.Flow.Nodes, oldId)
+ } else {
+ newNodes[node.ID] = node
+ }
+ }
+ oldStartNodeId := w.Data.Flow.Start.NodeID
+ if _, ok := oldToNewIdsMap[oldStartNodeId]; ok {
+ w.Data.Flow.Start.NodeID = oldToNewIdsMap[oldStartNodeId]
+ }
+
+ // Now go through all connections and map them with the new ids
+ for _, node := range newNodes {
+ for _, connection := range node.Connections {
+ if _, ok := oldToNewIdsMap[connection.NodeID]; ok {
+ connection.NodeID = oldToNewIdsMap[connection.NodeID]
+ }
+ }
+ }
+ w.Data.Flow.Nodes = newNodes
+ c.System().DB.Workflow.Put(newUser, &w)
+ }
+
err = c.System().DB.User.PutPw(newUser.ID, p.Password)
if err != nil {
return c.NoContent(http.StatusExpectationFailed)
@@ -902,6 +1035,44 @@ func GetProfilePhotoHandler(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
+// Check if a payment is required for current user for the workflow.
+// Return http OK if no payment is required or if payment is required and a payment a matching payment with status = "confirmed" is found
+func CheckForWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ sess := c.Session(true)
+ if sess == nil {
+ return c.NoContent(http.StatusNotFound)
+ }
+ workflowId := strings.TrimSpace(c.QueryParam("workflowId"))
+
+ if workflowId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ user, err := c.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ paymentRequired, err := payment.CheckIfWorkflowPaymentRequired(c, workflowId)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ if paymentRequired {
+ _, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowId, user.EthereumAddr, []string{model.PaymentStatusConfirmed})
+ if err != nil {
+ if err == strm.ErrNotFound {
+ return c.NoContent(http.StatusNotFound)
+ }
+ return c.NoContent(http.StatusBadRequest)
+ }
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+var errNoPaymentFound = errors.New("no payment for workflow")
+
func DocumentHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -918,13 +1089,44 @@ func DocumentHandler(e echo.Context) error {
if err != nil {
return c.String(http.StatusNotFound, err.Error())
}
+
docApp := getDocApp(c, sess, ID)
if docApp == nil {
- var usrDataItem *model.UserDataItem
- usrDataItem, err = c.System().DB.UserData.GetByWorkflow(sess, wf, false)
+ paymentRequired, err := payment.CheckIfWorkflowPaymentRequired(c, ID)
if err != nil {
return c.String(http.StatusNotFound, err.Error())
}
+
+ if paymentRequired {
+ sess := c.Session(false)
+ user, err := c.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ err = payment.RedeemPayment(c.System().DB.WorkflowPaymentsDB, wf.ID, user.EthereumAddr)
+ if err != nil {
+ log.Println("[redeemPayment] ", err.Error())
+ return c.String(http.StatusUnprocessableEntity, errNoPaymentFound.Error())
+ }
+ }
+
+ usrDataItem, _, err := c.System().DB.UserData.GetByWorkflow(sess, wf, false)
+ if err != nil {
+ if err != strm.ErrNotFound {
+ return c.String(http.StatusNotFound, err.Error())
+ }
+
+ usrDataItem = &model.UserDataItem{
+ WorkflowID: wf.ID,
+ Name: wf.Name,
+ Detail: wf.Detail,
+ }
+ err := c.System().DB.UserData.Put(sess, usrDataItem)
+ if err != nil {
+ return c.String(http.StatusInternalServerError, err.Error())
+ }
+ }
+
docApp, err = app.NewDocumentApp(usrDataItem, sess, c.System(), ID, sess.SessionDir())
if err != nil {
return c.String(http.StatusUnprocessableEntity, err.Error())
@@ -932,20 +1134,6 @@ func DocumentHandler(e echo.Context) error {
sess.Put("docApp_"+ID, docApp)
}
- err = checkIfWorkflowNeedsPayment(c.System().DB.WorkflowPaymentsDB, wf, sess.UserID())
- if err != nil {
- log.Println("[checkIfWorkflowNeedsPayment] ", err.Error())
- return c.String(http.StatusUnprocessableEntity, err.Error())
- }
-
- //check payment if not owner and not free
- if wf.Owner != sess.UserID() && wf.Price != 0 {
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowId(ID)
- if err != nil || workflowPaymentItem == nil {
- return c.String(http.StatusUnprocessableEntity, "no payment for workflow")
- }
- }
-
st, err = docApp.Current(nil)
if err == nil {
return c.JSON(http.StatusOK, map[string]interface{}{"name": docApp.WF().Name, "status": st})
@@ -960,18 +1148,6 @@ func DocumentHandler(e echo.Context) error {
}
}
-var errNoPaymentFound = errors.New("no payment for workflow")
-
-func checkIfWorkflowNeedsPayment(WorkflowPaymentsDB storm.WorkflowPaymentsDBInterface, wf *model.WorkflowItem, userId string) error {
- if wf.Owner != userId && wf.Price != 0 {
- workflowPaymentItem, err := WorkflowPaymentsDB.GetByWorkflowId(wf.ID)
- if err != nil || workflowPaymentItem == nil {
- return errNoPaymentFound
- }
- }
- return nil
-}
-
func DocumentDeleteHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -1084,17 +1260,6 @@ func DocumentNextHandler(e echo.Context) error {
return c.String(http.StatusBadRequest, err.Error())
}
- user, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
-
- // Invalidate payment by removing from WorkflowPaymentsDB once workflow is finished.
- err = DeletePaymentIfExists(c, ID, user.EthereumAddr)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
-
return c.JSON(http.StatusOK, map[string]interface{}{"id": dataID})
}
}
@@ -1116,24 +1281,6 @@ func DocumentNextHandler(e echo.Context) error {
return c.JSON(http.StatusOK, resData)
}
-// Remove payment from workflowPaymentsDB if exists
-func DeletePaymentIfExists(c *www.Context, workflowID, ethereumAddr string) error {
- workflowPaymentsDB := c.System().DB.WorkflowPaymentsDB
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowID, ethereumAddr)
- if err != nil {
- if err.Error() != "not found" { //if workflow is free or started by owner no payment will be found
- return err
- }
- } else {
- err = workflowPaymentsDB.Delete(workflowPaymentItem.TxHash)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
func DocumentPrevHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -1289,13 +1436,13 @@ func getDocApp(c *www.Context, sess *session.Session, ID string) *app.DocumentFl
func UserDocumentListHandler(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
contains := c.QueryParam("c")
settings := helpers.ReadReqSettings(c)
- items, err := c.System().DB.UserData.List(a, contains, settings, false)
+ items, err := c.System().DB.UserData.List(sess, contains, settings, false)
if err == nil && items != nil {
return c.JSON(http.StatusOK, items)
}
@@ -1304,12 +1451,12 @@ func UserDocumentListHandler(e echo.Context) error {
func UserDocumentGetHandler(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
id := c.Param("ID")
- items, err := c.System().DB.UserData.Get(a, id)
+ items, err := c.System().DB.UserData.Get(sess, id)
if err == nil && items != nil {
return c.JSON(http.StatusOK, items)
}
@@ -1418,7 +1565,7 @@ func UserDeleteHandler(e echo.Context) error {
//set workflow templates to deactivated
workflowDB := c.System().DB.Workflow
workflows, err := workflowDB.List(sess, "", map[string]interface{}{})
- if err != nil {
+ if err != nil && err.Error() != "not found" {
return c.NoContent(http.StatusInternalServerError)
}
for _, workflow := range workflows {
@@ -1832,16 +1979,16 @@ func AdminUserListHandler(e echo.Context) error {
func WorkflowSchema(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
id := c.Param("ID")
- wf, err := c.System().DB.Workflow.Get(a, id)
+ wf, err := c.System().DB.Workflow.Get(sess, id)
if err != nil {
return c.NoContent(http.StatusNotFound)
}
- fieldsAndRules := utils.GetAllFormFieldsWithRulesOf(wf.Data, a, c.System())
+ fieldsAndRules := utils.GetAllFormFieldsWithRulesOf(wf.Data, sess, c.System())
wfDetails := &struct {
*model.WorkflowItem
Data interface{} `json:"data"`
@@ -1854,8 +2001,8 @@ func WorkflowSchema(e echo.Context) error {
func WorkflowExecuteAtOnce(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
inputData, err := helpers.ParseDataFromReq(c)
@@ -1863,11 +2010,11 @@ func WorkflowExecuteAtOnce(e echo.Context) error {
return c.NoContent(http.StatusBadRequest)
}
id := c.Param("ID")
- wItem, err := c.System().DB.Workflow.Get(a, id)
+ wItem, err := c.System().DB.Workflow.Get(sess, id)
if err != nil || wItem.Data == nil {
return c.NoContent(http.StatusNotFound)
}
- err = app.ExecuteWorkflowAtOnce(c, a, wItem, inputData)
+ err = app.ExecuteWorkflowAtOnce(c, sess, wItem, inputData)
if err != nil {
if er, ok := err.(validate.ErrorMap); ok {
er.Translate(func(key string, args ...string) string {
@@ -1919,16 +2066,6 @@ func DeleteApiKeyHandler(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
-func ManagementListHandler(e echo.Context) error {
- c := e.(*www.Context)
- length := random(10, 40)
- var a []map[string]interface{}
- for i := 0; i < length; i++ {
- a = append(a, map[string]interface{}{"id": fmt.Sprintf("id %v", i), "owner": "owner", "consignmentID": "cons", "timestamp": "time", "signatory": "sig"})
- }
- return c.JSON(http.StatusOK, a)
-}
-
func random(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
diff --git a/main/handlers/api/handlers.go-e b/main/handlers/api/handlers.go-e
index 14bb46033..6deb952d8 100644
--- a/main/handlers/api/handlers.go-e
+++ b/main/handlers/api/handlers.go-e
@@ -12,6 +12,10 @@ import (
"strings"
"time"
+ workflow2 "git.proxeus.com/core/central/sys/workflow"
+
+ "git.proxeus.com/core/central/main/handlers/payment"
+
"git.proxeus.com/core/central/main/handlers/blockchain"
"git.proxeus.com/core/central/sys/utils"
@@ -48,7 +52,6 @@ import (
)
var filenameRegex = regexp.MustCompile(`^[^\s][\p{L}\d.,_\-&: ]{3,}[^\s]$`)
-var ServerVersion string
func html(c echo.Context, p string) error {
bts, err := sys.ReadAllFile(p)
@@ -249,7 +252,6 @@ func GetInit(e echo.Context) error {
if len(settings.PlatformDomain) == 0 {
settings.PlatformDomain = e.Request().Host
}
-
return c.JSON(http.StatusOK, map[string]interface{}{"settings": settings, "configured": configured})
}
@@ -305,20 +307,22 @@ func PostInit(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
-func ConfigHandler(e echo.Context) error {
- c := e.(*www.Context)
- sess := c.Session(false)
- var roles []model.RoleSet
- if sess != nil {
- roles = sess.AccessRights().RolesInRange()
+func ConfigHandler(version string) echo.HandlerFunc {
+ return func(e echo.Context) error {
+ c := e.(*www.Context)
+ sess := c.Session(false)
+ var roles []model.RoleSet
+ if sess != nil {
+ roles = sess.AccessRights().RolesInRange()
+ }
+ stngs := c.System().GetSettings()
+ return c.JSON(http.StatusOK, map[string]interface{}{
+ "roles": roles,
+ "blockchainNet": strings.Replace(stngs.BlockchainNet, "mainnet", "main", 1),
+ "blockchainProxeusFSAddress": stngs.BlockchainContractAddress,
+ "version": version,
+ })
}
- stngs := c.System().GetSettings()
- return c.JSON(http.StatusOK, map[string]interface{}{
- "roles": roles,
- "blockchainNet": strings.Replace(stngs.BlockchainNet, "mainnet", "main", 1),
- "blockchainProxeusFSAddress": stngs.BlockchainContractAddress,
- "version": ServerVersion,
- })
}
type loginForm struct {
@@ -468,10 +472,62 @@ func LoginWithWallet(c *www.Context, challenge, signature string) (bool, *model.
}
created = true
usr, err = c.System().DB.User.GetByBCAddress(address)
+ if err == nil && c.System().GetSettings().BlockchainNet == "ropsten" && c.System().GetSettings().AirdropEnabled == "true" {
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Println("airdrop recover with err ", r)
+ }
+ }()
+ blockchain.GiveTokens(address)
+ }()
+ }
}
return created, usr, err
}
+func GetSessionTokenHandler(e echo.Context) (err error) {
+ c := e.(*www.Context)
+
+ username, apiKey := c.BasicAuth()
+
+ if username == "" || apiKey == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ user, err := c.System().DB.User.APIKey(apiKey)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if user == nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if user.Email != username && user.EthereumAddr != username {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ //create a new session only if role, id or name has changed
+ sess := c.SessionWithUser(user)
+ if sess == nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ sess.Put("user", user)
+
+ c.Response().Header().Del("Set-Cookie")
+
+ return c.JSON(http.StatusOK, map[string]string{
+ "token": sess.ID(),
+ })
+}
+
+func DeleteSessionTokenHandler(e echo.Context) (err error) {
+ c := e.(*www.Context)
+ c.EndSession()
+ return c.NoContent(http.StatusOK)
+}
+
type TokenRequest struct {
Email string `json:"email" validate:"email=true,required=true"`
Token string `json:"token"`
@@ -545,38 +601,50 @@ func RegisterRequest(e echo.Context) (err error) {
stngs := c.System().GetSettings()
m.Role = stngs.DefaultRole
- if usr, err := c.System().DB.User.GetByEmail(m.Email); usr == nil {
- resetKey := m.Email + "_register"
- var token *TokenRequest
- err = c.System().Cache.Get(resetKey, &token)
+ if usr, _ := c.System().DB.User.GetByEmail(m.Email); usr != nil {
+ // always return ok if provided email was valid
+ // otherwise public users can test what email accounts exist
+ return c.NoContent(http.StatusOK)
+ }
+
+ resetKey := m.Email + "_register"
+ var token *TokenRequest
+
+ err = c.System().Cache.Get(resetKey, &token)
+ if err == nil {
+ return c.NoContent(http.StatusOK)
+ }
+
+ token = m
+ u2 := uuid.NewV4()
+ token.Token = u2.String()
+
+ if c.System().TestMode {
+ c.Response().Header().Set("X-Test-Token", token.Token)
+ } else {
+ err = c.System().EmailSender.Send(&email.Email{
+ From: stngs.EmailFrom,
+ To: []string{m.Email},
+ Subject: c.I18n().T("Register"),
+ Body: fmt.Sprintf(
+ "Hi there,\n\nplease proceed with your registration by visiting this link:\n%s\n\nIf you didn't request this, please ignore this email.\n\nProxeus",
+ helpers.AbsoluteURL(c, "/register/", token.Token),
+ ),
+ })
if err != nil {
- token = m
- u2 := uuid.NewV4()
- token.Token = u2.String()
- err = c.System().EmailSender.Send(&email.Email{
- From: stngs.EmailFrom,
- To: []string{m.Email},
- Subject: c.I18n().T("Register"),
- Body: fmt.Sprintf(
- "Hi there,\n\nplease proceed with your registration by visiting this link:\n%s\n\nIf you didn't request this, please ignore this email.\n\nProxeus",
- helpers.AbsoluteURL(c, "/register/", token.Token),
- ),
- })
- if err != nil {
- return c.NoContent(http.StatusExpectationFailed)
- }
- err = c.System().Cache.Put(resetKey, token)
- if err != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
- err = c.System().Cache.Put(token.Token, token)
- if err != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
+ return c.NoContent(http.StatusExpectationFailed)
}
}
- // always return ok if provided email was valid
- // otherwise public users can test what email accounts exist
+
+ err = c.System().Cache.Put(resetKey, token)
+ if err != nil {
+ return c.NoContent(http.StatusInternalServerError)
+ }
+ err = c.System().Cache.Put(token.Token, token)
+ if err != nil {
+ return c.NoContent(http.StatusInternalServerError)
+ }
+
return c.NoContent(http.StatusOK)
}
@@ -602,6 +670,71 @@ func Register(e echo.Context) error {
if err != nil {
return c.NoContent(http.StatusExpectationFailed)
}
+
+ // If some default workflows have to be assigned to the user, then clone them
+ workflowIds := strings.Split(c.System().GetSettings().DefaultWorkflowIds, ",")
+ workflows, err := c.System().DB.Workflow.GetList(root, workflowIds)
+ if err != nil {
+ log.Printf("Can't retrieve list of workflows (%v). Please check the ids exist. Error: %s", workflowIds, err.Error())
+ }
+ for _, workflow := range workflows {
+ w := workflow.Clone()
+ w.OwnerEthAddress = newUser.EthereumAddr
+ w.Owner = newUser.ID
+ newNodes := make(map[string]*workflow2.Node)
+ oldToNewIdsMap := make(map[string]string)
+ for oldId, node := range w.Data.Flow.Nodes {
+ if node.Type == "form" {
+ form, er := c.System().DB.Form.Get(root, node.ID)
+ if er != nil {
+ log.Println(err.Error())
+ }
+ f := form.Clone()
+ er = c.System().DB.Form.Put(newUser, &f)
+ if er != nil {
+ log.Println("can't put form" + err.Error())
+ }
+
+ oldToNewIdsMap[node.ID] = f.ID
+ node.ID = f.ID
+ newNodes[node.ID] = node
+ delete(w.Data.Flow.Nodes, oldId)
+
+ } else if node.Type == "template" {
+ template, er := c.System().DB.Template.Get(root, node.ID)
+ if er != nil {
+ log.Println(err.Error())
+ }
+ t := template.Clone()
+ er = c.System().DB.Template.Put(newUser, &t)
+ if er != nil {
+ log.Println("can't put template" + err.Error())
+ }
+ oldToNewIdsMap[node.ID] = t.ID
+ node.ID = t.ID
+ newNodes[node.ID] = node
+ delete(w.Data.Flow.Nodes, oldId)
+ } else {
+ newNodes[node.ID] = node
+ }
+ }
+ oldStartNodeId := w.Data.Flow.Start.NodeID
+ if _, ok := oldToNewIdsMap[oldStartNodeId]; ok {
+ w.Data.Flow.Start.NodeID = oldToNewIdsMap[oldStartNodeId]
+ }
+
+ // Now go through all connections and map them with the new ids
+ for _, node := range newNodes {
+ for _, connection := range node.Connections {
+ if _, ok := oldToNewIdsMap[connection.NodeID]; ok {
+ connection.NodeID = oldToNewIdsMap[connection.NodeID]
+ }
+ }
+ }
+ w.Data.Flow.Nodes = newNodes
+ c.System().DB.Workflow.Put(newUser, &w)
+ }
+
err = c.System().DB.User.PutPw(newUser.ID, p.Password)
if err != nil {
return c.NoContent(http.StatusExpectationFailed)
@@ -902,6 +1035,44 @@ func GetProfilePhotoHandler(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
+// Check if a payment is required for current user for the workflow.
+// Return http OK if no payment is required or if payment is required and a payment a matching payment with status = "confirmed" is found
+func CheckForWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ sess := c.Session(true)
+ if sess == nil {
+ return c.NoContent(http.StatusNotFound)
+ }
+ workflowId := strings.TrimSpace(c.QueryParam("workflowId"))
+
+ if workflowId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ user, err := c.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ paymentRequired, err := payment.CheckIfWorkflowPaymentRequired(c, workflowId)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ if paymentRequired {
+ _, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowId, user.EthereumAddr, []string{model.PaymentStatusConfirmed})
+ if err != nil {
+ if err == strm.ErrNotFound {
+ return c.NoContent(http.StatusNotFound)
+ }
+ return c.NoContent(http.StatusBadRequest)
+ }
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+var errNoPaymentFound = errors.New("no payment for workflow")
+
func DocumentHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -918,13 +1089,44 @@ func DocumentHandler(e echo.Context) error {
if err != nil {
return c.String(http.StatusNotFound, err.Error())
}
+
docApp := getDocApp(c, sess, ID)
if docApp == nil {
- var usrDataItem *model.UserDataItem
- usrDataItem, err = c.System().DB.UserData.GetByWorkflow(sess, wf, false)
+ paymentRequired, err := payment.CheckIfWorkflowPaymentRequired(c, ID)
if err != nil {
return c.String(http.StatusNotFound, err.Error())
}
+
+ if paymentRequired {
+ sess := c.Session(false)
+ user, err := c.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ err = payment.RedeemPayment(c.System().DB.WorkflowPaymentsDB, wf.ID, user.EthereumAddr)
+ if err != nil {
+ log.Println("[redeemPayment] ", err.Error())
+ return c.String(http.StatusUnprocessableEntity, errNoPaymentFound.Error())
+ }
+ }
+
+ usrDataItem, _, err := c.System().DB.UserData.GetByWorkflow(sess, wf, false)
+ if err != nil {
+ if err != strm.ErrNotFound {
+ return c.String(http.StatusNotFound, err.Error())
+ }
+
+ usrDataItem = &model.UserDataItem{
+ WorkflowID: wf.ID,
+ Name: wf.Name,
+ Detail: wf.Detail,
+ }
+ err := c.System().DB.UserData.Put(sess, usrDataItem)
+ if err != nil {
+ return c.String(http.StatusInternalServerError, err.Error())
+ }
+ }
+
docApp, err = app.NewDocumentApp(usrDataItem, sess, c.System(), ID, sess.SessionDir())
if err != nil {
return c.String(http.StatusUnprocessableEntity, err.Error())
@@ -932,20 +1134,6 @@ func DocumentHandler(e echo.Context) error {
sess.Put("docApp_"+ID, docApp)
}
- err = checkIfWorkflowNeedsPayment(c.System().DB.WorkflowPaymentsDB, wf, sess.UserID())
- if err != nil {
- log.Println("[checkIfWorkflowNeedsPayment] ", err.Error())
- return c.String(http.StatusUnprocessableEntity, err.Error())
- }
-
- //check payment if not owner and not free
- if wf.Owner != sess.UserID() && wf.Price != 0 {
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowId(ID)
- if err != nil || workflowPaymentItem == nil {
- return c.String(http.StatusUnprocessableEntity, "no payment for workflow")
- }
- }
-
st, err = docApp.Current(nil)
if err == nil {
return c.JSON(http.StatusOK, map[string]interface{}{"name": docApp.WF().Name, "status": st})
@@ -960,18 +1148,6 @@ func DocumentHandler(e echo.Context) error {
}
}
-var errNoPaymentFound = errors.New("no payment for workflow")
-
-func checkIfWorkflowNeedsPayment(WorkflowPaymentsDB storm.WorkflowPaymentsDBInterface, wf *model.WorkflowItem, userId string) error {
- if wf.Owner != userId && wf.Price != 0 {
- workflowPaymentItem, err := WorkflowPaymentsDB.GetByWorkflowId(wf.ID)
- if err != nil || workflowPaymentItem == nil {
- return errNoPaymentFound
- }
- }
- return nil
-}
-
func DocumentDeleteHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -1084,17 +1260,6 @@ func DocumentNextHandler(e echo.Context) error {
return c.String(http.StatusBadRequest, err.Error())
}
- user, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
-
- // Invalidate payment by removing from WorkflowPaymentsDB once workflow is finished.
- err = DeletePaymentIfExists(c, ID, user.EthereumAddr)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
-
return c.JSON(http.StatusOK, map[string]interface{}{"id": dataID})
}
}
@@ -1116,24 +1281,6 @@ func DocumentNextHandler(e echo.Context) error {
return c.JSON(http.StatusOK, resData)
}
-// Remove payment from workflowPaymentsDB if exists
-func DeletePaymentIfExists(c *www.Context, workflowID, ethereumAddr string) error {
- workflowPaymentsDB := c.System().DB.WorkflowPaymentsDB
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowID, ethereumAddr)
- if err != nil {
- if err.Error() != "not found" { //if workflow is free or started by owner no payment will be found
- return err
- }
- } else {
- err = workflowPaymentsDB.Delete(workflowPaymentItem.TxHash)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
func DocumentPrevHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -1289,13 +1436,13 @@ func getDocApp(c *www.Context, sess *session.Session, ID string) *app.DocumentFl
func UserDocumentListHandler(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
contains := c.QueryParam("c")
settings := helpers.ReadReqSettings(c)
- items, err := c.System().DB.UserData.List(a, contains, settings, false)
+ items, err := c.System().DB.UserData.List(sess, contains, settings, false)
if err == nil && items != nil {
return c.JSON(http.StatusOK, items)
}
@@ -1304,12 +1451,12 @@ func UserDocumentListHandler(e echo.Context) error {
func UserDocumentGetHandler(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
id := c.Param("ID")
- items, err := c.System().DB.UserData.Get(a, id)
+ items, err := c.System().DB.UserData.Get(sess, id)
if err == nil && items != nil {
return c.JSON(http.StatusOK, items)
}
@@ -1418,7 +1565,7 @@ func UserDeleteHandler(e echo.Context) error {
//set workflow templates to deactivated
workflowDB := c.System().DB.Workflow
workflows, err := workflowDB.List(sess, "", map[string]interface{}{})
- if err != nil {
+ if err != nil && err.Error() != "not found" {
return c.NoContent(http.StatusInternalServerError)
}
for _, workflow := range workflows {
@@ -1832,16 +1979,16 @@ func AdminUserListHandler(e echo.Context) error {
func WorkflowSchema(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
id := c.Param("ID")
- wf, err := c.System().DB.Workflow.Get(a, id)
+ wf, err := c.System().DB.Workflow.Get(sess, id)
if err != nil {
return c.NoContent(http.StatusNotFound)
}
- fieldsAndRules := utils.GetAllFormFieldsWithRulesOf(wf.Data, a, c.System())
+ fieldsAndRules := utils.GetAllFormFieldsWithRulesOf(wf.Data, sess, c.System())
wfDetails := &struct {
*model.WorkflowItem
Data interface{} `json:"data"`
@@ -1854,8 +2001,8 @@ func WorkflowSchema(e echo.Context) error {
func WorkflowExecuteAtOnce(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
inputData, err := helpers.ParseDataFromReq(c)
@@ -1863,11 +2010,11 @@ func WorkflowExecuteAtOnce(e echo.Context) error {
return c.NoContent(http.StatusBadRequest)
}
id := c.Param("ID")
- wItem, err := c.System().DB.Workflow.Get(a, id)
+ wItem, err := c.System().DB.Workflow.Get(sess, id)
if err != nil || wItem.Data == nil {
return c.NoContent(http.StatusNotFound)
}
- err = app.ExecuteWorkflowAtOnce(c, a, wItem, inputData)
+ err = app.ExecuteWorkflowAtOnce(c, sess, wItem, inputData)
if err != nil {
if er, ok := err.(validate.ErrorMap); ok {
er.Translate(func(key string, args ...string) string {
@@ -1919,16 +2066,6 @@ func DeleteApiKeyHandler(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
-func ManagementListHandler(e echo.Context) error {
- c := e.(*www.Context)
- length := random(10, 40)
- var a []map[string]interface{}
- for i := 0; i < length; i++ {
- a = append(a, map[string]interface{}{"id": fmt.Sprintf("id %v", i), "owner": "owner", "consignmentID": "cons", "timestamp": "time", "signatory": "sig"})
- }
- return c.JSON(http.StatusOK, a)
-}
-
func random(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
diff --git a/main/handlers/api/handlers_test.go b/main/handlers/api/handlers_test.go
deleted file mode 100644
index 378d8923f..000000000
--- a/main/handlers/api/handlers_test.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package api
-
-import (
- "errors"
- "testing"
-
- "github.com/golang/mock/gomock"
- "github.com/stretchr/testify/assert"
-
- "git.proxeus.com/core/central/main/www"
- "git.proxeus.com/core/central/sys"
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-)
-
-func TestCheckIfWorkflowNeedsPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfPaymentFound", func(t *testing.T) {
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5", WorkflowID: "3"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowId("3").Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfFreeWorkflow", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 0}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfOwnerStartsWorkflow", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "33")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldFailIfNoPaymentFound", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowId("3").Return(nil, nil).Times(1)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.EqualError(t, result, errNoPaymentFound.Error())
- })
-}
-
-func TestDeletePaymentIfExists(t *testing.T) {
-
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("DeletePaymentIfExistsShouldSucceedIfPaymentDeleted", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(workflowPaymentItem, nil).Times(1)
- workflowPaymentsDBMock.EXPECT().Delete(gomock.Eq(workflowPaymentItem.TxHash)).Return(nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.NoError(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldSucceedIfNoPaymentToDelete", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- err := errors.New("not found")
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(nil, err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.NoError(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldFailOnGetError", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- err := errors.New("some error")
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(nil, err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.Error(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldFailOnDeleteError", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(workflowPaymentItem, nil).Times(1)
-
- err := errors.New("some error")
-
- workflowPaymentsDBMock.EXPECT().Delete(gomock.Eq(workflowPaymentItem.TxHash)).Return(err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.Error(t, result)
- })
-
-}
diff --git a/main/handlers/api/handlers_test.go-e b/main/handlers/api/handlers_test.go-e
deleted file mode 100644
index 378d8923f..000000000
--- a/main/handlers/api/handlers_test.go-e
+++ /dev/null
@@ -1,142 +0,0 @@
-package api
-
-import (
- "errors"
- "testing"
-
- "github.com/golang/mock/gomock"
- "github.com/stretchr/testify/assert"
-
- "git.proxeus.com/core/central/main/www"
- "git.proxeus.com/core/central/sys"
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-)
-
-func TestCheckIfWorkflowNeedsPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfPaymentFound", func(t *testing.T) {
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5", WorkflowID: "3"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowId("3").Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfFreeWorkflow", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 0}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfOwnerStartsWorkflow", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "33")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldFailIfNoPaymentFound", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowId("3").Return(nil, nil).Times(1)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.EqualError(t, result, errNoPaymentFound.Error())
- })
-}
-
-func TestDeletePaymentIfExists(t *testing.T) {
-
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("DeletePaymentIfExistsShouldSucceedIfPaymentDeleted", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(workflowPaymentItem, nil).Times(1)
- workflowPaymentsDBMock.EXPECT().Delete(gomock.Eq(workflowPaymentItem.TxHash)).Return(nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.NoError(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldSucceedIfNoPaymentToDelete", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- err := errors.New("not found")
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(nil, err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.NoError(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldFailOnGetError", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- err := errors.New("some error")
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(nil, err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.Error(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldFailOnDeleteError", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(workflowPaymentItem, nil).Times(1)
-
- err := errors.New("some error")
-
- workflowPaymentsDBMock.EXPECT().Delete(gomock.Eq(workflowPaymentItem.TxHash)).Return(err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.Error(t, result)
- })
-
-}
diff --git a/main/handlers/blockchain/airdrop.go b/main/handlers/blockchain/airdrop.go
new file mode 100644
index 000000000..603fc4817
--- /dev/null
+++ b/main/handlers/blockchain/airdrop.go
@@ -0,0 +1,144 @@
+package blockchain
+
+import (
+ "context"
+ "encoding/json"
+ "io/ioutil"
+ "log"
+ "math/big"
+ "os"
+ "strconv"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+
+ "git.proxeus.com/core/central/main/config"
+
+ "github.com/ethereum/go-ethereum/accounts/abi/bind"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethclient"
+
+ "git.proxeus.com/core/central/main/ethglue"
+)
+
+const etherUnit = 1000000000000000000.0
+
+var conn *ethclient.Client
+var nonceManager ethglue.NonceManager
+
+var mu sync.Mutex
+
+func GiveTokens(toWallet string) {
+ var err error
+ conn, err = ethglue.Dial(config.Config.EthClientURL)
+ if err != nil {
+ log.Panic("[airdrop] Failed to connect to the Ethereum client:", err)
+ }
+ nonceManager.OnDial(conn)
+
+ type Web3Keystore struct {
+ Address string `json:"address"`
+ }
+ var keystore Web3Keystore
+
+ keystoreJSON, err := ioutil.ReadFile(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+ err = json.Unmarshal(keystoreJSON, &keystore)
+ if err != nil {
+ log.Panic("[airdrop] Failed to parse keystore:", err)
+ }
+
+ address := keystore.Address
+ nonceManager.OnAccountChange(address)
+ mu.Lock()
+ defer mu.Unlock()
+ FreeXES(toWallet)
+ FreeEth(toWallet)
+}
+
+// FreeXES sends ropsten XES to given wallet address
+func FreeXES(walletAddress string) {
+ log.Println("[airdrop] [Ropsten] Prepare XES for addr:", walletAddress)
+ amount := new(big.Int)
+ f, err := strconv.ParseFloat(config.Config.AirdropAmountXES, 64)
+
+ amount.SetInt64(int64(f * etherUnit))
+
+ // make transfer
+ token, err := NewToken(common.HexToAddress(config.Config.XESContractAddress), conn)
+ if err != nil {
+ log.Panic("[airdrop] Failed to instantiate a Token contract:", err)
+ }
+
+ keystorereader, err := os.Open(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+
+ keystorekey, err := ioutil.ReadFile(config.Config.AirdropWalletkey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore key:", err)
+ }
+
+ // Create an authorized transactor and spend Amount XES
+ auth, err := bind.NewTransactor(keystorereader, string(keystorekey[:len(keystorekey)-1]))
+ if err != nil {
+ log.Panic("[airdrop] Failed to create authorized transactor:", err)
+ }
+
+ auth.Nonce = nonceManager.NextNonce()
+ tx, err := token.Transfer(auth, common.HexToAddress(walletAddress), amount)
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic("[airdrop] Failed to request token transfer:", err)
+ }
+ log.Println("[airdrop] [Ropsten] Sending XES with tx:", tx.Hash().String())
+}
+
+func FreeEth(walletAddress string) {
+ log.Println("[airdrop] [Ropsten] Prepare ETH for addr:", walletAddress)
+ amount := new(big.Int)
+ f, err := strconv.ParseFloat(config.Config.AirdropAmountEther, 64)
+ amount.SetInt64(int64(f * etherUnit))
+
+ var gasLimit = uint64(21000)
+ gasPrice, err := conn.SuggestGasPrice(context.Background())
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic(err)
+ }
+
+ keystoreJSON, err := ioutil.ReadFile(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+
+ keystorekey, err := ioutil.ReadFile(config.Config.AirdropWalletkey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore key:", err)
+ }
+
+ unlockedKey, err := keystore.DecryptKey(keystoreJSON, string(keystorekey[:len(keystorekey)-1]))
+ if err != nil {
+ log.Panic("[airdrop] Failed to create authorized transactor:", err)
+ }
+
+ nonce := nonceManager.NextNonce()
+ tx := types.NewTransaction(nonce.Uint64(), common.HexToAddress(walletAddress), amount, gasLimit, gasPrice, nil)
+ // chainid 3 = ropsten, see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids
+ signedTx, err := types.SignTx(tx, types.NewEIP155Signer(big.NewInt(3)), unlockedKey.PrivateKey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to sign transaction:", err)
+ }
+
+ err = conn.SendTransaction(context.Background(), signedTx)
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic("[airdrop] Failed to send transaction:", err)
+ }
+
+ log.Println("[airdrop] [Ropsten] Sending ETH with tx:", signedTx.Hash().String())
+}
diff --git a/main/handlers/blockchain/airdrop.go-e b/main/handlers/blockchain/airdrop.go-e
new file mode 100644
index 000000000..603fc4817
--- /dev/null
+++ b/main/handlers/blockchain/airdrop.go-e
@@ -0,0 +1,144 @@
+package blockchain
+
+import (
+ "context"
+ "encoding/json"
+ "io/ioutil"
+ "log"
+ "math/big"
+ "os"
+ "strconv"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+
+ "git.proxeus.com/core/central/main/config"
+
+ "github.com/ethereum/go-ethereum/accounts/abi/bind"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethclient"
+
+ "git.proxeus.com/core/central/main/ethglue"
+)
+
+const etherUnit = 1000000000000000000.0
+
+var conn *ethclient.Client
+var nonceManager ethglue.NonceManager
+
+var mu sync.Mutex
+
+func GiveTokens(toWallet string) {
+ var err error
+ conn, err = ethglue.Dial(config.Config.EthClientURL)
+ if err != nil {
+ log.Panic("[airdrop] Failed to connect to the Ethereum client:", err)
+ }
+ nonceManager.OnDial(conn)
+
+ type Web3Keystore struct {
+ Address string `json:"address"`
+ }
+ var keystore Web3Keystore
+
+ keystoreJSON, err := ioutil.ReadFile(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+ err = json.Unmarshal(keystoreJSON, &keystore)
+ if err != nil {
+ log.Panic("[airdrop] Failed to parse keystore:", err)
+ }
+
+ address := keystore.Address
+ nonceManager.OnAccountChange(address)
+ mu.Lock()
+ defer mu.Unlock()
+ FreeXES(toWallet)
+ FreeEth(toWallet)
+}
+
+// FreeXES sends ropsten XES to given wallet address
+func FreeXES(walletAddress string) {
+ log.Println("[airdrop] [Ropsten] Prepare XES for addr:", walletAddress)
+ amount := new(big.Int)
+ f, err := strconv.ParseFloat(config.Config.AirdropAmountXES, 64)
+
+ amount.SetInt64(int64(f * etherUnit))
+
+ // make transfer
+ token, err := NewToken(common.HexToAddress(config.Config.XESContractAddress), conn)
+ if err != nil {
+ log.Panic("[airdrop] Failed to instantiate a Token contract:", err)
+ }
+
+ keystorereader, err := os.Open(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+
+ keystorekey, err := ioutil.ReadFile(config.Config.AirdropWalletkey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore key:", err)
+ }
+
+ // Create an authorized transactor and spend Amount XES
+ auth, err := bind.NewTransactor(keystorereader, string(keystorekey[:len(keystorekey)-1]))
+ if err != nil {
+ log.Panic("[airdrop] Failed to create authorized transactor:", err)
+ }
+
+ auth.Nonce = nonceManager.NextNonce()
+ tx, err := token.Transfer(auth, common.HexToAddress(walletAddress), amount)
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic("[airdrop] Failed to request token transfer:", err)
+ }
+ log.Println("[airdrop] [Ropsten] Sending XES with tx:", tx.Hash().String())
+}
+
+func FreeEth(walletAddress string) {
+ log.Println("[airdrop] [Ropsten] Prepare ETH for addr:", walletAddress)
+ amount := new(big.Int)
+ f, err := strconv.ParseFloat(config.Config.AirdropAmountEther, 64)
+ amount.SetInt64(int64(f * etherUnit))
+
+ var gasLimit = uint64(21000)
+ gasPrice, err := conn.SuggestGasPrice(context.Background())
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic(err)
+ }
+
+ keystoreJSON, err := ioutil.ReadFile(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+
+ keystorekey, err := ioutil.ReadFile(config.Config.AirdropWalletkey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore key:", err)
+ }
+
+ unlockedKey, err := keystore.DecryptKey(keystoreJSON, string(keystorekey[:len(keystorekey)-1]))
+ if err != nil {
+ log.Panic("[airdrop] Failed to create authorized transactor:", err)
+ }
+
+ nonce := nonceManager.NextNonce()
+ tx := types.NewTransaction(nonce.Uint64(), common.HexToAddress(walletAddress), amount, gasLimit, gasPrice, nil)
+ // chainid 3 = ropsten, see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids
+ signedTx, err := types.SignTx(tx, types.NewEIP155Signer(big.NewInt(3)), unlockedKey.PrivateKey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to sign transaction:", err)
+ }
+
+ err = conn.SendTransaction(context.Background(), signedTx)
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic("[airdrop] Failed to send transaction:", err)
+ }
+
+ log.Println("[airdrop] [Ropsten] Sending ETH with tx:", signedTx.Hash().String())
+}
diff --git a/main/handlers/blockchain/listener_test.go b/main/handlers/blockchain/listener_test.go
deleted file mode 100644
index f2311d8f2..000000000
--- a/main/handlers/blockchain/listener_test.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package blockchain
-
-import (
- "log"
- "math/big"
- "testing"
-
- "github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/core/types"
- "github.com/golang/mock/gomock"
-
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-)
-
-type (
- workflowPaymentItemMatcher struct {
- transactionHash string
- from string
- to string
- xes uint64
- }
-)
-
-func (me *workflowPaymentItemMatcher) Matches(x interface{}) bool {
- workflowPaymentItem, ok := x.(*model.WorkflowPaymentItem)
- if !ok {
- log.Fatal("workflowPaymentItemMatcher cast error")
- }
- return workflowPaymentItem.WorkflowID == "" &&
- me.transactionHash == workflowPaymentItem.TxHash &&
- me.from == workflowPaymentItem.From &&
- me.to == workflowPaymentItem.To &&
- me.xes == workflowPaymentItem.Xes
-}
-func (me *workflowPaymentItemMatcher) String() string {
- return "workflowPaymentItem needs to match"
-}
-
-func TestCheckIfWorkflowNeedsPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent1Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(1), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent130Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(130), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent100000000000000000Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(100000000000000000), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerShowsOverflowError", func(t *testing.T) {
- xesTmp := big.NewInt(1000000000000000000)
- xes := xesTmp.Mul(xesTmp, big.NewInt(1000000000000000000))
- err := runTest(mockCtrl, xes, false)
- if err != xesOverflowError {
- if err != nil {
- log.Fatal("expected xesOverflowError but got: ", err.Error())
- }
- log.Fatal("expected xesOverflowError but got: nil")
- }
- })
-}
-
-func runTest(mockCtrl *gomock.Controller, xesAmount *big.Int, addPaymentExpected bool) error {
- adapterMock := NewMockadapter(mockCtrl)
- adapterMock.EXPECT().eventFromLog(gomock.Any(), gomock.Any(), gomock.Eq("Transfer")).Return(nil).Times(1)
-
- transactionHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
- from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
- to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
-
- xesUint := xesAmount.Uint64()
-
- xes := xesAmount.Mul(xesAmount, big.NewInt(1000000000000000000))
-
- workflowPaymentItemMatcher := &workflowPaymentItemMatcher{
- transactionHash: transactionHash,
- from: from,
- to: to,
- xes: xesUint,
- }
- paymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- if addPaymentExpected {
- paymentsDBMock.EXPECT().Add(workflowPaymentItemMatcher)
- }
-
- event := &XesMainTokenTransfer{
- Value: xes,
- FromAddress: common.HexToAddress(from),
- ToAddress: common.HexToAddress(to),
- }
-
- listener := &Paymentlistener{
- xesAdapter: adapterMock,
- workflowPaymentsDB: paymentsDBMock,
- }
-
- ethLog := &types.Log{
- TxHash: common.HexToHash(transactionHash),
- }
-
- return listener.eventsHandler(ethLog, event)
-}
diff --git a/main/handlers/blockchain/listener_test.go-e b/main/handlers/blockchain/listener_test.go-e
deleted file mode 100644
index f2311d8f2..000000000
--- a/main/handlers/blockchain/listener_test.go-e
+++ /dev/null
@@ -1,115 +0,0 @@
-package blockchain
-
-import (
- "log"
- "math/big"
- "testing"
-
- "github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/core/types"
- "github.com/golang/mock/gomock"
-
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-)
-
-type (
- workflowPaymentItemMatcher struct {
- transactionHash string
- from string
- to string
- xes uint64
- }
-)
-
-func (me *workflowPaymentItemMatcher) Matches(x interface{}) bool {
- workflowPaymentItem, ok := x.(*model.WorkflowPaymentItem)
- if !ok {
- log.Fatal("workflowPaymentItemMatcher cast error")
- }
- return workflowPaymentItem.WorkflowID == "" &&
- me.transactionHash == workflowPaymentItem.TxHash &&
- me.from == workflowPaymentItem.From &&
- me.to == workflowPaymentItem.To &&
- me.xes == workflowPaymentItem.Xes
-}
-func (me *workflowPaymentItemMatcher) String() string {
- return "workflowPaymentItem needs to match"
-}
-
-func TestCheckIfWorkflowNeedsPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent1Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(1), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent130Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(130), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent100000000000000000Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(100000000000000000), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerShowsOverflowError", func(t *testing.T) {
- xesTmp := big.NewInt(1000000000000000000)
- xes := xesTmp.Mul(xesTmp, big.NewInt(1000000000000000000))
- err := runTest(mockCtrl, xes, false)
- if err != xesOverflowError {
- if err != nil {
- log.Fatal("expected xesOverflowError but got: ", err.Error())
- }
- log.Fatal("expected xesOverflowError but got: nil")
- }
- })
-}
-
-func runTest(mockCtrl *gomock.Controller, xesAmount *big.Int, addPaymentExpected bool) error {
- adapterMock := NewMockadapter(mockCtrl)
- adapterMock.EXPECT().eventFromLog(gomock.Any(), gomock.Any(), gomock.Eq("Transfer")).Return(nil).Times(1)
-
- transactionHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
- from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
- to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
-
- xesUint := xesAmount.Uint64()
-
- xes := xesAmount.Mul(xesAmount, big.NewInt(1000000000000000000))
-
- workflowPaymentItemMatcher := &workflowPaymentItemMatcher{
- transactionHash: transactionHash,
- from: from,
- to: to,
- xes: xesUint,
- }
- paymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- if addPaymentExpected {
- paymentsDBMock.EXPECT().Add(workflowPaymentItemMatcher)
- }
-
- event := &XesMainTokenTransfer{
- Value: xes,
- FromAddress: common.HexToAddress(from),
- ToAddress: common.HexToAddress(to),
- }
-
- listener := &Paymentlistener{
- xesAdapter: adapterMock,
- workflowPaymentsDB: paymentsDBMock,
- }
-
- ethLog := &types.Log{
- TxHash: common.HexToHash(transactionHash),
- }
-
- return listener.eventsHandler(ethLog, event)
-}
diff --git a/main/handlers/blockchain/payment_listener.go b/main/handlers/blockchain/payment_listener.go
new file mode 100644
index 000000000..abc207f68
--- /dev/null
+++ b/main/handlers/blockchain/payment_listener.go
@@ -0,0 +1,162 @@
+package blockchain
+
+import (
+ "context"
+ "errors"
+ "log"
+ "math/big"
+ "time"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+
+ "git.proxeus.com/core/central/main/ethglue"
+ "git.proxeus.com/core/central/sys/db/storm"
+
+ strm "github.com/asdine/storm"
+)
+
+type (
+ listener struct {
+ logs chan types.Log
+ ethWebSocketURL string
+ ethURL string
+ sub ethereum.Subscription
+ }
+ PaymentListener struct {
+ listener
+ workflowPaymentsDB storm.WorkflowPaymentsDBInterface
+ xesAdapter adapter
+ }
+)
+
+func NewPaymentListener(xesAdapter adapter, ethWebSocketURL, ethURL string, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) *PaymentListener {
+ me := &PaymentListener{}
+ me.xesAdapter = xesAdapter
+ me.ethWebSocketURL = ethWebSocketURL
+ me.ethURL = ethURL
+ me.workflowPaymentsDB = workflowPaymentsDB
+ me.logs = make(chan types.Log, 200)
+ return me
+}
+
+func (me *PaymentListener) Listen(ctx context.Context) {
+ var readyCh <-chan struct{}
+
+ for {
+ readyCh = me.ethConnectWebSocketsAsync(ctx)
+ select {
+ case <-readyCh:
+ log.Println("[paymentlistener] listen on contract started. contract address: ", me.xesAdapter.getContractAddress())
+ reconnect := me.listenLoop(ctx)
+ if !reconnect {
+ log.Printf("[paymentlistener][eventHandler] finished")
+ return
+ }
+ case <-ctx.Done():
+ log.Printf("[paymentlistener][eventHandler] done")
+ return
+ }
+ }
+ return
+}
+
+func (me *PaymentListener) listenLoop(ctx context.Context) (shouldReconnect bool) {
+ for {
+ select {
+ case <-ctx.Done():
+ return false
+ case err, ok := <-me.sub.Err():
+ if !ok {
+ return true
+ }
+ log.Println("ERROR sub", err)
+ return true
+ case vLog, ok := <-me.logs:
+ if !ok {
+ return true
+ }
+ event := new(XesMainTokenTransfer)
+ err := me.eventsHandler(&vLog, event)
+ if err != nil {
+ if err != strm.ErrNotFound { //ErrNotFound already logged in eventsHandler
+ if err == xesOverflowError {
+ log.Fatal("[blockchain][listener] overflow err: ", err.Error())
+ }
+ log.Println("[blockchain][listener] err: ", err.Error())
+ }
+ }
+ }
+ }
+}
+
+func (me *PaymentListener) ethConnectWebSocketsAsync(ctx context.Context) <-chan struct{} {
+
+ filterAddresses := []common.Address{common.HexToAddress(me.xesAdapter.getContractAddress())}
+
+ readyCh := make(chan struct{})
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ var err error
+ ethwsconn, err := ethglue.DialContext(ctx, me.ethWebSocketURL)
+ if err != nil {
+ log.Printf("failed to dial for eth events, will retry (%s)\n", err)
+ time.Sleep(time.Second * 4)
+ continue
+ }
+ query := ethereum.FilterQuery{
+ Addresses: filterAddresses,
+ }
+ ctx, cancel := context.WithTimeout(ctx, time.Duration(10*time.Second))
+ me.sub, err = ethwsconn.SubscribeFilterLogs(ctx, query, me.logs)
+ cancel()
+ if err != nil {
+ log.Printf("failed to subscribe for eth events, will retry (%s)\n", err)
+ time.Sleep(time.Second * 4)
+ continue
+ }
+ // success!
+ readyCh <- struct{}{}
+ return
+ }
+ }
+ }()
+ return readyCh
+}
+
+var xesOverflowError = errors.New("overflow on xes event")
+
+func (me *PaymentListener) eventsHandler(lg *types.Log, event *XesMainTokenTransfer) error {
+ log.Printf("[PaymentListener][eventHandler] txHash: %s, value: %s, %v",
+ lg.TxHash.String(), event.Value.String(), lg)
+ err := me.xesAdapter.eventFromLog(event, lg, "Transfer")
+ if err != nil {
+ return err
+ }
+
+ bigXes := event.Value.Div(event.Value, big.NewInt(1000000000000000000)) //to xes-ether
+
+ if !bigXes.IsUint64() {
+ log.Println("[PaymentListener][eventHandler] error overflow on transfer event value:", event.Value)
+ return xesOverflowError
+ }
+
+ err = me.workflowPaymentsDB.ConfirmPayment(lg.TxHash.String(), event.FromAddress.String(), event.ToAddress.String(), bigXes.Uint64())
+ if err != nil {
+ if err == strm.ErrNotFound {
+ log.Printf(" [PaymentListener][eventHandler] info: no matching payment found for txHash: %s, reason: %s", lg.TxHash.String(), err.Error())
+ return err
+ }
+
+ log.Printf(" [PaymentListener][eventHandler] err: workflowPaymentsDB.ConfirmPayment for txHash: %s, err: %s", lg.TxHash.String(), err.Error())
+ return err
+ }
+ log.Println("[PaymentListener][eventHandler] confirmed payment with hash: ", lg.TxHash.String())
+
+ return nil
+}
diff --git a/main/handlers/blockchain/payment_listener.go-e b/main/handlers/blockchain/payment_listener.go-e
new file mode 100644
index 000000000..abc207f68
--- /dev/null
+++ b/main/handlers/blockchain/payment_listener.go-e
@@ -0,0 +1,162 @@
+package blockchain
+
+import (
+ "context"
+ "errors"
+ "log"
+ "math/big"
+ "time"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+
+ "git.proxeus.com/core/central/main/ethglue"
+ "git.proxeus.com/core/central/sys/db/storm"
+
+ strm "github.com/asdine/storm"
+)
+
+type (
+ listener struct {
+ logs chan types.Log
+ ethWebSocketURL string
+ ethURL string
+ sub ethereum.Subscription
+ }
+ PaymentListener struct {
+ listener
+ workflowPaymentsDB storm.WorkflowPaymentsDBInterface
+ xesAdapter adapter
+ }
+)
+
+func NewPaymentListener(xesAdapter adapter, ethWebSocketURL, ethURL string, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) *PaymentListener {
+ me := &PaymentListener{}
+ me.xesAdapter = xesAdapter
+ me.ethWebSocketURL = ethWebSocketURL
+ me.ethURL = ethURL
+ me.workflowPaymentsDB = workflowPaymentsDB
+ me.logs = make(chan types.Log, 200)
+ return me
+}
+
+func (me *PaymentListener) Listen(ctx context.Context) {
+ var readyCh <-chan struct{}
+
+ for {
+ readyCh = me.ethConnectWebSocketsAsync(ctx)
+ select {
+ case <-readyCh:
+ log.Println("[paymentlistener] listen on contract started. contract address: ", me.xesAdapter.getContractAddress())
+ reconnect := me.listenLoop(ctx)
+ if !reconnect {
+ log.Printf("[paymentlistener][eventHandler] finished")
+ return
+ }
+ case <-ctx.Done():
+ log.Printf("[paymentlistener][eventHandler] done")
+ return
+ }
+ }
+ return
+}
+
+func (me *PaymentListener) listenLoop(ctx context.Context) (shouldReconnect bool) {
+ for {
+ select {
+ case <-ctx.Done():
+ return false
+ case err, ok := <-me.sub.Err():
+ if !ok {
+ return true
+ }
+ log.Println("ERROR sub", err)
+ return true
+ case vLog, ok := <-me.logs:
+ if !ok {
+ return true
+ }
+ event := new(XesMainTokenTransfer)
+ err := me.eventsHandler(&vLog, event)
+ if err != nil {
+ if err != strm.ErrNotFound { //ErrNotFound already logged in eventsHandler
+ if err == xesOverflowError {
+ log.Fatal("[blockchain][listener] overflow err: ", err.Error())
+ }
+ log.Println("[blockchain][listener] err: ", err.Error())
+ }
+ }
+ }
+ }
+}
+
+func (me *PaymentListener) ethConnectWebSocketsAsync(ctx context.Context) <-chan struct{} {
+
+ filterAddresses := []common.Address{common.HexToAddress(me.xesAdapter.getContractAddress())}
+
+ readyCh := make(chan struct{})
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ var err error
+ ethwsconn, err := ethglue.DialContext(ctx, me.ethWebSocketURL)
+ if err != nil {
+ log.Printf("failed to dial for eth events, will retry (%s)\n", err)
+ time.Sleep(time.Second * 4)
+ continue
+ }
+ query := ethereum.FilterQuery{
+ Addresses: filterAddresses,
+ }
+ ctx, cancel := context.WithTimeout(ctx, time.Duration(10*time.Second))
+ me.sub, err = ethwsconn.SubscribeFilterLogs(ctx, query, me.logs)
+ cancel()
+ if err != nil {
+ log.Printf("failed to subscribe for eth events, will retry (%s)\n", err)
+ time.Sleep(time.Second * 4)
+ continue
+ }
+ // success!
+ readyCh <- struct{}{}
+ return
+ }
+ }
+ }()
+ return readyCh
+}
+
+var xesOverflowError = errors.New("overflow on xes event")
+
+func (me *PaymentListener) eventsHandler(lg *types.Log, event *XesMainTokenTransfer) error {
+ log.Printf("[PaymentListener][eventHandler] txHash: %s, value: %s, %v",
+ lg.TxHash.String(), event.Value.String(), lg)
+ err := me.xesAdapter.eventFromLog(event, lg, "Transfer")
+ if err != nil {
+ return err
+ }
+
+ bigXes := event.Value.Div(event.Value, big.NewInt(1000000000000000000)) //to xes-ether
+
+ if !bigXes.IsUint64() {
+ log.Println("[PaymentListener][eventHandler] error overflow on transfer event value:", event.Value)
+ return xesOverflowError
+ }
+
+ err = me.workflowPaymentsDB.ConfirmPayment(lg.TxHash.String(), event.FromAddress.String(), event.ToAddress.String(), bigXes.Uint64())
+ if err != nil {
+ if err == strm.ErrNotFound {
+ log.Printf(" [PaymentListener][eventHandler] info: no matching payment found for txHash: %s, reason: %s", lg.TxHash.String(), err.Error())
+ return err
+ }
+
+ log.Printf(" [PaymentListener][eventHandler] err: workflowPaymentsDB.ConfirmPayment for txHash: %s, err: %s", lg.TxHash.String(), err.Error())
+ return err
+ }
+ log.Println("[PaymentListener][eventHandler] confirmed payment with hash: ", lg.TxHash.String())
+
+ return nil
+}
diff --git a/main/handlers/blockchain/payment_listener_test.go b/main/handlers/blockchain/payment_listener_test.go
new file mode 100644
index 000000000..ba0b383b2
--- /dev/null
+++ b/main/handlers/blockchain/payment_listener_test.go
@@ -0,0 +1,315 @@
+package blockchain
+
+import (
+ "errors"
+ "math/big"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/golang/mock/gomock"
+
+ strm "github.com/asdine/storm"
+
+ "git.proxeus.com/core/central/sys/model"
+
+ "git.proxeus.com/core/central/sys/db/storm"
+)
+
+var errCleanupTestData = errors.New("db data has not been cleanup up after finishing tests")
+
+func removePaymentIfExists(workflowPaymentsDB storm.WorkflowPaymentsDBInterface,
+ persistedPaymentItem **model.WorkflowPaymentItem) {
+
+ if *persistedPaymentItem != nil {
+ err := workflowPaymentsDB.Remove(*persistedPaymentItem)
+ if err != nil {
+ panic(err.Error())
+ }
+ }
+}
+
+func TestPaymentEventHandling(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ workflowPaymentsDB, err := storm.NewWorkflowPaymentDB(".test_data")
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ payments, err := workflowPaymentsDB.All()
+ if err != nil {
+ panic(err)
+ }
+ if len(payments) != 0 {
+ panic(errCleanupTestData)
+ }
+
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir, storm.WorkflowPaymentDB))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(".test_data")
+ if err != nil {
+ panic(err.Error())
+ }
+
+ }()
+
+ t.Run("ShouldSetPendingPaymentWithTxHashToConfirmed", func(t *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "1"
+ paymentTxHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ eventTxHash := paymentTxHash
+ expectedStatus := model.PaymentStatusConfirmed
+ xesAmount := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID, paymentTxHash, from,
+ to, model.PaymentStatusPending, xesAmount)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldSetCreatedPaymentWithoutTxHashToConfirmed", func(t *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "2"
+ eventTxHash := "0x94420cb493b721c627feb8f911df8546bba0f911cb4433dfabd4c9c65012593c"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ expectedStatus := model.PaymentStatusConfirmed
+ xesAmount := big.NewInt(100000000000000000)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusCreated, xesAmount)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldIgnoreAlreadyRedeemedPayment", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "3"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ expectedStatus := model.PaymentStatusRedeemed
+ xesAmount := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != strm.ErrNotFound {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", strm.ErrNotFound, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", strm.ErrNotFound)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+ })
+
+ t.Run("ShouldIgnorePaymentOnNotMatchingXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "4"
+ paymentTxHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ eventTxHash := paymentTxHash
+ expectedStatus := model.PaymentStatusCreated
+ xesAmount := big.NewInt(2)
+ xesAmountEvent := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmountEvent, eventTxHash, paymentID, paymentTxHash, from,
+ to, model.PaymentStatusCreated, xesAmount)
+ if err != strm.ErrNotFound {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", strm.ErrNotFound, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", strm.ErrNotFound)
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldReturnErrorOverflowOnBigXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "5"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+
+ xesTmp := big.NewInt(1000000000000000000)
+ xesAmount := xesTmp.Mul(xesTmp, big.NewInt(1000000000000000000))
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != xesOverflowError {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", xesOverflowError, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", xesOverflowError)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldReturnErrorOverflowOnNegativeXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "6"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+
+ xesAmount := big.NewInt(-1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != xesOverflowError {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", xesOverflowError, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", xesOverflowError)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+}
+
+func runTest(mockCtrl *gomock.Controller, workflowPaymentsDB *storm.WorkflowPaymentsDB,
+ eventXesAmount *big.Int, eventTxHash, paymentID, paymentTxHash, from, to, status string, xesAmount *big.Int) error {
+
+ adapterMock := NewMockadapter(mockCtrl)
+ adapterMock.EXPECT().eventFromLog(gomock.Any(), gomock.Any(), gomock.Eq("Transfer")).Return(nil).Times(1)
+
+ newPaymentItem := &model.WorkflowPaymentItem{
+ ID: paymentID,
+ From: from,
+ To: to,
+ CreatedAt: time.Now(),
+ Status: status,
+ Xes: xesAmount.Uint64(),
+ WorkflowID: "1",
+ }
+
+ if paymentTxHash != "" {
+ newPaymentItem.TxHash = paymentTxHash
+ }
+
+ err := workflowPaymentsDB.Save(newPaymentItem)
+ if err != nil {
+ return err
+ }
+
+ eventXes := xesAmount.Mul(eventXesAmount, big.NewInt(1000000000000000000))
+
+ event := &XesMainTokenTransfer{
+ Value: eventXes,
+ FromAddress: common.HexToAddress(from),
+ ToAddress: common.HexToAddress(to),
+ }
+
+ listener := &PaymentListener{
+ xesAdapter: adapterMock,
+ workflowPaymentsDB: workflowPaymentsDB,
+ }
+
+ ethLog := &types.Log{
+ TxHash: common.HexToHash(eventTxHash),
+ }
+
+ return listener.eventsHandler(ethLog, event)
+}
diff --git a/main/handlers/blockchain/payment_listener_test.go-e b/main/handlers/blockchain/payment_listener_test.go-e
new file mode 100644
index 000000000..ba0b383b2
--- /dev/null
+++ b/main/handlers/blockchain/payment_listener_test.go-e
@@ -0,0 +1,315 @@
+package blockchain
+
+import (
+ "errors"
+ "math/big"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/golang/mock/gomock"
+
+ strm "github.com/asdine/storm"
+
+ "git.proxeus.com/core/central/sys/model"
+
+ "git.proxeus.com/core/central/sys/db/storm"
+)
+
+var errCleanupTestData = errors.New("db data has not been cleanup up after finishing tests")
+
+func removePaymentIfExists(workflowPaymentsDB storm.WorkflowPaymentsDBInterface,
+ persistedPaymentItem **model.WorkflowPaymentItem) {
+
+ if *persistedPaymentItem != nil {
+ err := workflowPaymentsDB.Remove(*persistedPaymentItem)
+ if err != nil {
+ panic(err.Error())
+ }
+ }
+}
+
+func TestPaymentEventHandling(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ workflowPaymentsDB, err := storm.NewWorkflowPaymentDB(".test_data")
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ payments, err := workflowPaymentsDB.All()
+ if err != nil {
+ panic(err)
+ }
+ if len(payments) != 0 {
+ panic(errCleanupTestData)
+ }
+
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir, storm.WorkflowPaymentDB))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(".test_data")
+ if err != nil {
+ panic(err.Error())
+ }
+
+ }()
+
+ t.Run("ShouldSetPendingPaymentWithTxHashToConfirmed", func(t *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "1"
+ paymentTxHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ eventTxHash := paymentTxHash
+ expectedStatus := model.PaymentStatusConfirmed
+ xesAmount := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID, paymentTxHash, from,
+ to, model.PaymentStatusPending, xesAmount)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldSetCreatedPaymentWithoutTxHashToConfirmed", func(t *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "2"
+ eventTxHash := "0x94420cb493b721c627feb8f911df8546bba0f911cb4433dfabd4c9c65012593c"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ expectedStatus := model.PaymentStatusConfirmed
+ xesAmount := big.NewInt(100000000000000000)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusCreated, xesAmount)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldIgnoreAlreadyRedeemedPayment", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "3"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ expectedStatus := model.PaymentStatusRedeemed
+ xesAmount := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != strm.ErrNotFound {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", strm.ErrNotFound, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", strm.ErrNotFound)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+ })
+
+ t.Run("ShouldIgnorePaymentOnNotMatchingXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "4"
+ paymentTxHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ eventTxHash := paymentTxHash
+ expectedStatus := model.PaymentStatusCreated
+ xesAmount := big.NewInt(2)
+ xesAmountEvent := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmountEvent, eventTxHash, paymentID, paymentTxHash, from,
+ to, model.PaymentStatusCreated, xesAmount)
+ if err != strm.ErrNotFound {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", strm.ErrNotFound, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", strm.ErrNotFound)
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldReturnErrorOverflowOnBigXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "5"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+
+ xesTmp := big.NewInt(1000000000000000000)
+ xesAmount := xesTmp.Mul(xesTmp, big.NewInt(1000000000000000000))
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != xesOverflowError {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", xesOverflowError, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", xesOverflowError)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldReturnErrorOverflowOnNegativeXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "6"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+
+ xesAmount := big.NewInt(-1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != xesOverflowError {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", xesOverflowError, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", xesOverflowError)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+}
+
+func runTest(mockCtrl *gomock.Controller, workflowPaymentsDB *storm.WorkflowPaymentsDB,
+ eventXesAmount *big.Int, eventTxHash, paymentID, paymentTxHash, from, to, status string, xesAmount *big.Int) error {
+
+ adapterMock := NewMockadapter(mockCtrl)
+ adapterMock.EXPECT().eventFromLog(gomock.Any(), gomock.Any(), gomock.Eq("Transfer")).Return(nil).Times(1)
+
+ newPaymentItem := &model.WorkflowPaymentItem{
+ ID: paymentID,
+ From: from,
+ To: to,
+ CreatedAt: time.Now(),
+ Status: status,
+ Xes: xesAmount.Uint64(),
+ WorkflowID: "1",
+ }
+
+ if paymentTxHash != "" {
+ newPaymentItem.TxHash = paymentTxHash
+ }
+
+ err := workflowPaymentsDB.Save(newPaymentItem)
+ if err != nil {
+ return err
+ }
+
+ eventXes := xesAmount.Mul(eventXesAmount, big.NewInt(1000000000000000000))
+
+ event := &XesMainTokenTransfer{
+ Value: eventXes,
+ FromAddress: common.HexToAddress(from),
+ ToAddress: common.HexToAddress(to),
+ }
+
+ listener := &PaymentListener{
+ xesAdapter: adapterMock,
+ workflowPaymentsDB: workflowPaymentsDB,
+ }
+
+ ethLog := &types.Log{
+ TxHash: common.HexToHash(eventTxHash),
+ }
+
+ return listener.eventsHandler(ethLog, event)
+}
diff --git a/main/handlers/blockchain/listener.go b/main/handlers/blockchain/signature_listener.go
similarity index 56%
rename from main/handlers/blockchain/listener.go
rename to main/handlers/blockchain/signature_listener.go
index 859e6cbc6..588c58402 100644
--- a/main/handlers/blockchain/listener.go
+++ b/main/handlers/blockchain/signature_listener.go
@@ -3,13 +3,9 @@ package blockchain
import (
"context"
"encoding/hex"
- "errors"
"log"
- "math/big"
"time"
- "git.proxeus.com/core/central/sys/email"
-
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
@@ -18,21 +14,10 @@ import (
"git.proxeus.com/core/central/main/ethglue"
"git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
+ "git.proxeus.com/core/central/sys/email"
)
type (
- listener struct {
- logs chan types.Log
- ethWebSocketURL string
- ethURL string
- sub ethereum.Subscription
- }
- Paymentlistener struct {
- listener
- workflowPaymentsDB storm.WorkflowPaymentsDBInterface
- xesAdapter adapter
- }
Signaturelistener struct {
listener
signatureRequestsDB storm.SignatureRequestsDB
@@ -45,127 +30,9 @@ type (
}
)
-func NewPaymentListener(xesAdapter adapter, ethWebSocketURL, ethURL string, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) *Paymentlistener {
- me := &Paymentlistener{}
- me.xesAdapter = xesAdapter
- me.ethWebSocketURL = ethWebSocketURL
- me.ethURL = ethURL
- me.workflowPaymentsDB = workflowPaymentsDB
- me.logs = make(chan types.Log, 200)
- return me
-}
-
-func (me *Paymentlistener) Listen(ctx context.Context) {
- var readyCh <-chan struct{}
-
- for {
- readyCh = me.ethConnectWebSocketsAsync(ctx)
- select {
- case <-readyCh:
- log.Println("[paymentlistener] listen on contract started. contract address: ", me.xesAdapter.getContractAddress())
- reconnect := me.listenLoop(ctx)
- if !reconnect {
- log.Printf("[paymentlistener][eventHandler] finished")
- return
- }
- case <-ctx.Done():
- log.Printf("[paymentlistener][eventHandler] done")
- return
- }
- }
- return
-}
-
-func (me *Paymentlistener) listenLoop(ctx context.Context) (shouldReconnect bool) {
- for {
- select {
- case <-ctx.Done():
- return false
- case err, ok := <-me.sub.Err():
- if !ok {
- return true
- }
- log.Println("ERROR sub", err)
- return true
- case vLog, ok := <-me.logs:
- if !ok {
- return true
- }
- event := new(XesMainTokenTransfer)
- err := me.eventsHandler(&vLog, event)
- if err != nil {
- if err == xesOverflowError {
- log.Fatal("[blockchain][listener] ", err.Error())
- }
- log.Println("[blockchain][listener] ", err.Error())
- }
- }
- }
-}
-
-func (me *Paymentlistener) ethConnectWebSocketsAsync(ctx context.Context) <-chan struct{} {
-
- filterAddresses := []common.Address{common.HexToAddress(me.xesAdapter.getContractAddress())}
-
- readyCh := make(chan struct{})
- go func() {
- for {
- select {
- case <-ctx.Done():
- return
- default:
- var err error
- ethwsconn, err := ethglue.DialContext(ctx, me.ethWebSocketURL)
- if err != nil {
- log.Printf("failed to dial for eth events, will retry (%s)\n", err)
- continue
- }
- query := ethereum.FilterQuery{
- Addresses: filterAddresses,
- }
- ctx, cancel := context.WithTimeout(ctx, time.Duration(10*time.Second))
- me.sub, err = ethwsconn.SubscribeFilterLogs(ctx, query, me.logs)
- cancel()
- if err != nil {
- log.Printf("failed to subscribe for eth events, will retry (%s)\n", err)
- time.Sleep(time.Second * 4)
- continue
- }
- // success!
- readyCh <- struct{}{}
- return
- }
- }
- }()
- return readyCh
-}
-
-var xesOverflowError = errors.New("overflow on xes event")
-
-func (me *Paymentlistener) eventsHandler(lg *types.Log, event *XesMainTokenTransfer) error {
- log.Printf("[paymentlistener][eventHandler] txHash: %s, value: %s, %v",
- lg.TxHash.String(), event.Value.String(), lg)
- if err := me.xesAdapter.eventFromLog(event, lg, "Transfer"); err != nil {
- return err
- }
-
- bigXes := event.Value.Div(event.Value, big.NewInt(1000000000000000000)) //to xes-ether
-
- if !bigXes.IsUint64() {
- log.Println(" error overflow on transfer event value:", event.Value)
- return xesOverflowError
- }
-
- item := &model.WorkflowPaymentItem{
- TxHash: lg.TxHash.String(),
- Xes: bigXes.Uint64(),
- From: event.FromAddress.String(),
- To: event.ToAddress.String(),
- }
- return me.workflowPaymentsDB.Add(item)
-}
+func NewSignatureListener(ethWebSocketURL, ethURL, BlockchainContractAddress string, SignatureRequestsDB *storm.SignatureRequestsDB,
+ UserDB storm.UserDBInterface, EmailSender email.EmailSender, ProxeusFSABI abi.ABI, domain string) *Signaturelistener {
-func NewSignatureListener(ethWebSocketURL, ethURL, BlockchainContractAddress string, SignatureRequestsDB *storm.SignatureRequestsDB, UserDB storm.UserDBInterface, EmailSender email.EmailSender, ProxeusFSABI abi.ABI, domain string) *Signaturelistener {
me := &Signaturelistener{}
me.BlockchainContractAddress = BlockchainContractAddress
me.ethWebSocketURL = ethWebSocketURL
diff --git a/main/handlers/blockchain/listener.go-e b/main/handlers/blockchain/signature_listener.go-e
similarity index 56%
rename from main/handlers/blockchain/listener.go-e
rename to main/handlers/blockchain/signature_listener.go-e
index 859e6cbc6..588c58402 100644
--- a/main/handlers/blockchain/listener.go-e
+++ b/main/handlers/blockchain/signature_listener.go-e
@@ -3,13 +3,9 @@ package blockchain
import (
"context"
"encoding/hex"
- "errors"
"log"
- "math/big"
"time"
- "git.proxeus.com/core/central/sys/email"
-
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
@@ -18,21 +14,10 @@ import (
"git.proxeus.com/core/central/main/ethglue"
"git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
+ "git.proxeus.com/core/central/sys/email"
)
type (
- listener struct {
- logs chan types.Log
- ethWebSocketURL string
- ethURL string
- sub ethereum.Subscription
- }
- Paymentlistener struct {
- listener
- workflowPaymentsDB storm.WorkflowPaymentsDBInterface
- xesAdapter adapter
- }
Signaturelistener struct {
listener
signatureRequestsDB storm.SignatureRequestsDB
@@ -45,127 +30,9 @@ type (
}
)
-func NewPaymentListener(xesAdapter adapter, ethWebSocketURL, ethURL string, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) *Paymentlistener {
- me := &Paymentlistener{}
- me.xesAdapter = xesAdapter
- me.ethWebSocketURL = ethWebSocketURL
- me.ethURL = ethURL
- me.workflowPaymentsDB = workflowPaymentsDB
- me.logs = make(chan types.Log, 200)
- return me
-}
-
-func (me *Paymentlistener) Listen(ctx context.Context) {
- var readyCh <-chan struct{}
-
- for {
- readyCh = me.ethConnectWebSocketsAsync(ctx)
- select {
- case <-readyCh:
- log.Println("[paymentlistener] listen on contract started. contract address: ", me.xesAdapter.getContractAddress())
- reconnect := me.listenLoop(ctx)
- if !reconnect {
- log.Printf("[paymentlistener][eventHandler] finished")
- return
- }
- case <-ctx.Done():
- log.Printf("[paymentlistener][eventHandler] done")
- return
- }
- }
- return
-}
-
-func (me *Paymentlistener) listenLoop(ctx context.Context) (shouldReconnect bool) {
- for {
- select {
- case <-ctx.Done():
- return false
- case err, ok := <-me.sub.Err():
- if !ok {
- return true
- }
- log.Println("ERROR sub", err)
- return true
- case vLog, ok := <-me.logs:
- if !ok {
- return true
- }
- event := new(XesMainTokenTransfer)
- err := me.eventsHandler(&vLog, event)
- if err != nil {
- if err == xesOverflowError {
- log.Fatal("[blockchain][listener] ", err.Error())
- }
- log.Println("[blockchain][listener] ", err.Error())
- }
- }
- }
-}
-
-func (me *Paymentlistener) ethConnectWebSocketsAsync(ctx context.Context) <-chan struct{} {
-
- filterAddresses := []common.Address{common.HexToAddress(me.xesAdapter.getContractAddress())}
-
- readyCh := make(chan struct{})
- go func() {
- for {
- select {
- case <-ctx.Done():
- return
- default:
- var err error
- ethwsconn, err := ethglue.DialContext(ctx, me.ethWebSocketURL)
- if err != nil {
- log.Printf("failed to dial for eth events, will retry (%s)\n", err)
- continue
- }
- query := ethereum.FilterQuery{
- Addresses: filterAddresses,
- }
- ctx, cancel := context.WithTimeout(ctx, time.Duration(10*time.Second))
- me.sub, err = ethwsconn.SubscribeFilterLogs(ctx, query, me.logs)
- cancel()
- if err != nil {
- log.Printf("failed to subscribe for eth events, will retry (%s)\n", err)
- time.Sleep(time.Second * 4)
- continue
- }
- // success!
- readyCh <- struct{}{}
- return
- }
- }
- }()
- return readyCh
-}
-
-var xesOverflowError = errors.New("overflow on xes event")
-
-func (me *Paymentlistener) eventsHandler(lg *types.Log, event *XesMainTokenTransfer) error {
- log.Printf("[paymentlistener][eventHandler] txHash: %s, value: %s, %v",
- lg.TxHash.String(), event.Value.String(), lg)
- if err := me.xesAdapter.eventFromLog(event, lg, "Transfer"); err != nil {
- return err
- }
-
- bigXes := event.Value.Div(event.Value, big.NewInt(1000000000000000000)) //to xes-ether
-
- if !bigXes.IsUint64() {
- log.Println(" error overflow on transfer event value:", event.Value)
- return xesOverflowError
- }
-
- item := &model.WorkflowPaymentItem{
- TxHash: lg.TxHash.String(),
- Xes: bigXes.Uint64(),
- From: event.FromAddress.String(),
- To: event.ToAddress.String(),
- }
- return me.workflowPaymentsDB.Add(item)
-}
+func NewSignatureListener(ethWebSocketURL, ethURL, BlockchainContractAddress string, SignatureRequestsDB *storm.SignatureRequestsDB,
+ UserDB storm.UserDBInterface, EmailSender email.EmailSender, ProxeusFSABI abi.ABI, domain string) *Signaturelistener {
-func NewSignatureListener(ethWebSocketURL, ethURL, BlockchainContractAddress string, SignatureRequestsDB *storm.SignatureRequestsDB, UserDB storm.UserDBInterface, EmailSender email.EmailSender, ProxeusFSABI abi.ABI, domain string) *Signaturelistener {
me := &Signaturelistener{}
me.BlockchainContractAddress = BlockchainContractAddress
me.ethWebSocketURL = ethWebSocketURL
diff --git a/main/handlers/payment/handler.go b/main/handlers/payment/handler.go
new file mode 100644
index 000000000..ffb9f40af
--- /dev/null
+++ b/main/handlers/payment/handler.go
@@ -0,0 +1,276 @@
+package payment
+
+import (
+ "errors"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ uuid "github.com/satori/go.uuid"
+
+ "git.proxeus.com/core/central/sys/db/storm"
+
+ strm "github.com/asdine/storm"
+
+ "github.com/labstack/echo"
+
+ "git.proxeus.com/core/central/main/www"
+ "git.proxeus.com/core/central/sys/model"
+)
+
+var errNotAuthorized = errors.New("user not authorized")
+
+type createPaymentRequest struct {
+ WorkflowId string `json:"workflowId"`
+}
+
+//create a payment for a workflow
+func CreateWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ createPaymentRequest := &createPaymentRequest{}
+ err = c.Bind(&createPaymentRequest)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ createPaymentRequest.WorkflowId = strings.TrimSpace(createPaymentRequest.WorkflowId)
+
+ if createPaymentRequest.WorkflowId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ workflow, err := c.System().DB.Workflow.Get(c.Session(false), createPaymentRequest.WorkflowId)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ id := uuid.NewV4().String()
+
+ payment := &model.WorkflowPaymentItem{
+ ID: id,
+ Xes: workflow.Price,
+ From: user.EthereumAddr,
+ To: workflow.OwnerEthAddress,
+ Status: model.PaymentStatusCreated,
+ CreatedAt: time.Now(),
+ WorkflowID: createPaymentRequest.WorkflowId,
+ }
+
+ err = c.System().DB.WorkflowPaymentsDB.Save(payment)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+// Gets a payment by Id
+// Payment is only returned if the from address == the user sending the request
+func GetWorkflowPaymentById(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := c.Param("paymentId")
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ payment, err := c.System().DB.WorkflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ log.Println("[GetWorkflowPaymentById] getUserPaymentById err: ", err.Error())
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if payment.From != user.EthereumAddr {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+// Returns payment with the given txHash and status.
+// Payment is only returned if the from address == the user sending the request
+func GetWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ txHash := c.QueryParam("txHash")
+ status := c.QueryParam("status")
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ payment, err := getWorkflowPayment(c.System().DB.WorkflowPaymentsDB, txHash, user.EthereumAddr, status)
+ if err != nil {
+ if err == strm.ErrNotFound {
+ return c.NoContent(http.StatusNotFound)
+ }
+ log.Println("[GetWorkflowPayment] GetByTxHashAndStatusAndFromEthAddress err: ", err.Error())
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+var errRequiredParamMissing = errors.New("required parameter missing")
+
+func getWorkflowPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, txHash,
+ ethAddr, status string) (*model.WorkflowPaymentItem, error) {
+
+ if txHash == "" {
+ log.Printf("[GetWorkflowPayment] bad request, either provide paymentId, txHash or workflowId")
+ return nil, errRequiredParamMissing
+ }
+
+ payment, err := workflowPaymentsDB.GetByTxHashAndStatusAndFromEthAddress(txHash, status, ethAddr)
+ if err != nil {
+ log.Println("[GetWorkflowPayment] GetByTxHashAndStatusAndFromEthAddress err: ", err.Error())
+ return nil, err
+ }
+
+ log.Printf("[workflowHandler][GetWorkflowPayment] ID: %s, txHash: %s", payment.ID, payment.TxHash)
+
+ return payment, nil
+}
+
+type updatePaymentPendingRequest struct {
+ TxHash string `json:"txHash"`
+}
+
+var errTxHashEmpty = errors.New("no txHash given")
+
+// Set a workflow payment from status created to status pending
+func UpdateWorkflowPaymentPending(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ updatePaymentRequest := &updatePaymentPendingRequest{}
+ err = c.Bind(&updatePaymentRequest)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] UpdateWorkflowPayment bind err: %s", err.Error())
+ return err
+ }
+
+ err = updateWorkflowPaymentPending(c.System().DB.WorkflowPaymentsDB, paymentId, updatePaymentRequest.TxHash, user.EthereumAddr)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] err: %s", err.Error())
+ if err == errTxHashEmpty {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+func updateWorkflowPaymentPending(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, paymentId, txHash, ethAddr string) error {
+ txHash = strings.TrimSpace(txHash)
+ if txHash == "" {
+ return errTxHashEmpty
+ }
+
+ err := workflowPaymentsDB.Update(paymentId, model.PaymentStatusPending, txHash, ethAddr)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] WorkflowPaymentsDB.Update err: %s", err.Error())
+ return err
+ }
+
+ return nil
+}
+
+// Set status of workflow from created to cancelled
+func CancelWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ user, err := getUser(c)
+ if err != nil {
+ return errNotAuthorized
+ }
+
+ err = cancelWorkflowPayment(c.System().DB.WorkflowPaymentsDB, paymentId, user.EthereumAddr)
+ if err != nil {
+ return c.String(http.StatusNotFound, err.Error())
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+func cancelWorkflowPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, paymentId, ethAddr string) error {
+ return workflowPaymentsDB.Cancel(paymentId, ethAddr)
+}
+
+// Set the payment status from confirmed to redeemed
+func RedeemPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, workflowId, ethAddr string) error {
+ return workflowPaymentsDB.Redeem(workflowId, ethAddr)
+}
+
+//returns a bool indicating whether a payment is required for the user for a workflow
+func CheckIfWorkflowPaymentRequired(c *www.Context, workflowId string) (bool, error) {
+ sess := c.Session(false)
+
+ workflow, err := c.System().DB.Workflow.Get(sess, workflowId)
+ if err != nil {
+ return true, err
+ }
+
+ _, alreadyStarted, err := c.System().DB.UserData.GetByWorkflow(sess, workflow, false)
+ if err != nil {
+ if err != strm.ErrNotFound {
+ return true, nil
+ }
+ //if workflow not found (strm.ErrNotFound ) still check with isPaymentRequired
+ }
+
+ return isPaymentRequired(alreadyStarted, workflow, c.Session(false).UserID()), nil
+}
+
+func isPaymentRequired(alreadyStarted bool, workflow *model.WorkflowItem, userId string) bool {
+ return !alreadyStarted && workflow.Owner != userId && workflow.Price != 0
+}
+
+// Set Payment for a workflow to status = Deleted. Only for superadmin
+func DeleteWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ if paymentId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ err := c.System().DB.WorkflowPaymentsDB.Delete(paymentId)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+// List all Payment. Only for superadmin and debugging purposes
+func ListPayments(e echo.Context) error {
+ c := e.(*www.Context)
+
+ payments, err := c.System().DB.WorkflowPaymentsDB.All()
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payments)
+}
+
+func getUser(c *www.Context) (*model.User, error) {
+ sess := c.Session(false)
+ return c.System().DB.User.Get(sess, sess.UserID())
+}
diff --git a/main/handlers/payment/handler.go-e b/main/handlers/payment/handler.go-e
new file mode 100644
index 000000000..ffb9f40af
--- /dev/null
+++ b/main/handlers/payment/handler.go-e
@@ -0,0 +1,276 @@
+package payment
+
+import (
+ "errors"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ uuid "github.com/satori/go.uuid"
+
+ "git.proxeus.com/core/central/sys/db/storm"
+
+ strm "github.com/asdine/storm"
+
+ "github.com/labstack/echo"
+
+ "git.proxeus.com/core/central/main/www"
+ "git.proxeus.com/core/central/sys/model"
+)
+
+var errNotAuthorized = errors.New("user not authorized")
+
+type createPaymentRequest struct {
+ WorkflowId string `json:"workflowId"`
+}
+
+//create a payment for a workflow
+func CreateWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ createPaymentRequest := &createPaymentRequest{}
+ err = c.Bind(&createPaymentRequest)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ createPaymentRequest.WorkflowId = strings.TrimSpace(createPaymentRequest.WorkflowId)
+
+ if createPaymentRequest.WorkflowId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ workflow, err := c.System().DB.Workflow.Get(c.Session(false), createPaymentRequest.WorkflowId)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ id := uuid.NewV4().String()
+
+ payment := &model.WorkflowPaymentItem{
+ ID: id,
+ Xes: workflow.Price,
+ From: user.EthereumAddr,
+ To: workflow.OwnerEthAddress,
+ Status: model.PaymentStatusCreated,
+ CreatedAt: time.Now(),
+ WorkflowID: createPaymentRequest.WorkflowId,
+ }
+
+ err = c.System().DB.WorkflowPaymentsDB.Save(payment)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+// Gets a payment by Id
+// Payment is only returned if the from address == the user sending the request
+func GetWorkflowPaymentById(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := c.Param("paymentId")
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ payment, err := c.System().DB.WorkflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ log.Println("[GetWorkflowPaymentById] getUserPaymentById err: ", err.Error())
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if payment.From != user.EthereumAddr {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+// Returns payment with the given txHash and status.
+// Payment is only returned if the from address == the user sending the request
+func GetWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ txHash := c.QueryParam("txHash")
+ status := c.QueryParam("status")
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ payment, err := getWorkflowPayment(c.System().DB.WorkflowPaymentsDB, txHash, user.EthereumAddr, status)
+ if err != nil {
+ if err == strm.ErrNotFound {
+ return c.NoContent(http.StatusNotFound)
+ }
+ log.Println("[GetWorkflowPayment] GetByTxHashAndStatusAndFromEthAddress err: ", err.Error())
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+var errRequiredParamMissing = errors.New("required parameter missing")
+
+func getWorkflowPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, txHash,
+ ethAddr, status string) (*model.WorkflowPaymentItem, error) {
+
+ if txHash == "" {
+ log.Printf("[GetWorkflowPayment] bad request, either provide paymentId, txHash or workflowId")
+ return nil, errRequiredParamMissing
+ }
+
+ payment, err := workflowPaymentsDB.GetByTxHashAndStatusAndFromEthAddress(txHash, status, ethAddr)
+ if err != nil {
+ log.Println("[GetWorkflowPayment] GetByTxHashAndStatusAndFromEthAddress err: ", err.Error())
+ return nil, err
+ }
+
+ log.Printf("[workflowHandler][GetWorkflowPayment] ID: %s, txHash: %s", payment.ID, payment.TxHash)
+
+ return payment, nil
+}
+
+type updatePaymentPendingRequest struct {
+ TxHash string `json:"txHash"`
+}
+
+var errTxHashEmpty = errors.New("no txHash given")
+
+// Set a workflow payment from status created to status pending
+func UpdateWorkflowPaymentPending(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ updatePaymentRequest := &updatePaymentPendingRequest{}
+ err = c.Bind(&updatePaymentRequest)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] UpdateWorkflowPayment bind err: %s", err.Error())
+ return err
+ }
+
+ err = updateWorkflowPaymentPending(c.System().DB.WorkflowPaymentsDB, paymentId, updatePaymentRequest.TxHash, user.EthereumAddr)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] err: %s", err.Error())
+ if err == errTxHashEmpty {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+func updateWorkflowPaymentPending(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, paymentId, txHash, ethAddr string) error {
+ txHash = strings.TrimSpace(txHash)
+ if txHash == "" {
+ return errTxHashEmpty
+ }
+
+ err := workflowPaymentsDB.Update(paymentId, model.PaymentStatusPending, txHash, ethAddr)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] WorkflowPaymentsDB.Update err: %s", err.Error())
+ return err
+ }
+
+ return nil
+}
+
+// Set status of workflow from created to cancelled
+func CancelWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ user, err := getUser(c)
+ if err != nil {
+ return errNotAuthorized
+ }
+
+ err = cancelWorkflowPayment(c.System().DB.WorkflowPaymentsDB, paymentId, user.EthereumAddr)
+ if err != nil {
+ return c.String(http.StatusNotFound, err.Error())
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+func cancelWorkflowPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, paymentId, ethAddr string) error {
+ return workflowPaymentsDB.Cancel(paymentId, ethAddr)
+}
+
+// Set the payment status from confirmed to redeemed
+func RedeemPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, workflowId, ethAddr string) error {
+ return workflowPaymentsDB.Redeem(workflowId, ethAddr)
+}
+
+//returns a bool indicating whether a payment is required for the user for a workflow
+func CheckIfWorkflowPaymentRequired(c *www.Context, workflowId string) (bool, error) {
+ sess := c.Session(false)
+
+ workflow, err := c.System().DB.Workflow.Get(sess, workflowId)
+ if err != nil {
+ return true, err
+ }
+
+ _, alreadyStarted, err := c.System().DB.UserData.GetByWorkflow(sess, workflow, false)
+ if err != nil {
+ if err != strm.ErrNotFound {
+ return true, nil
+ }
+ //if workflow not found (strm.ErrNotFound ) still check with isPaymentRequired
+ }
+
+ return isPaymentRequired(alreadyStarted, workflow, c.Session(false).UserID()), nil
+}
+
+func isPaymentRequired(alreadyStarted bool, workflow *model.WorkflowItem, userId string) bool {
+ return !alreadyStarted && workflow.Owner != userId && workflow.Price != 0
+}
+
+// Set Payment for a workflow to status = Deleted. Only for superadmin
+func DeleteWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ if paymentId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ err := c.System().DB.WorkflowPaymentsDB.Delete(paymentId)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+// List all Payment. Only for superadmin and debugging purposes
+func ListPayments(e echo.Context) error {
+ c := e.(*www.Context)
+
+ payments, err := c.System().DB.WorkflowPaymentsDB.All()
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payments)
+}
+
+func getUser(c *www.Context) (*model.User, error) {
+ sess := c.Session(false)
+ return c.System().DB.User.Get(sess, sess.UserID())
+}
diff --git a/main/handlers/payment/handler_test.go b/main/handlers/payment/handler_test.go
new file mode 100644
index 000000000..de84dfb75
--- /dev/null
+++ b/main/handlers/payment/handler_test.go
@@ -0,0 +1,549 @@
+package payment
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+
+ strm "github.com/asdine/storm"
+
+ "git.proxeus.com/core/central/main/www"
+ "git.proxeus.com/core/central/sys"
+ "git.proxeus.com/core/central/sys/db/storm"
+ "git.proxeus.com/core/central/sys/model"
+
+ sysSess "git.proxeus.com/core/central/sys/session"
+)
+
+func setupPaymentRequestTest(httpMethod, targetUrl, body string) (*www.Context, *httptest.ResponseRecorder, *model.User, *model.User) {
+ e := echo.New()
+ req := httptest.NewRequest(httpMethod, targetUrl, strings.NewReader(body))
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ rec := httptest.NewRecorder()
+ c := e.NewContext(req, rec)
+
+ sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
+ c.Set("_session_store", sessionStore)
+ sysSession := &sysSess.Session{}
+ sysSession.SetUserID("1")
+
+ c.Set("sys.session", sysSession)
+ wwwContext := &www.Context{Context: c}
+ wwwContext.SetRequest(req)
+
+ user := &model.User{}
+ user.EthereumAddr = "0x00"
+
+ ownerUser := &model.User{}
+ ownerUser.EthereumAddr = "0x3"
+
+ return wwwContext, rec, user, ownerUser
+}
+
+type paymentResponse struct {
+ Id string `json:id`
+}
+
+func TestCreateWorkflowPayment(t *testing.T) {
+
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ body := `{"workflowId":"552a2f0e-c6c4-403b-8aaf-2d9ebf55eb8f"}`
+
+ wwwContext, rec, user, _ := setupPaymentRequestTest(http.MethodPost, "/api/admin/payments", body)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ workflow := &model.WorkflowItem{Price: 2000000000000000000}
+ workflow.Owner = user.EthereumAddr
+ workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
+ workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock, Workflow: workflowDBMock}
+ www.SetSystem(system)
+
+ t.Run("ShouldCreatePaymentItem", func(t *testing.T) {
+ if assert.NoError(t, CreateWorkflowPayment(wwwContext)) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+
+ var response = paymentResponse{}
+ err := json.Unmarshal(rec.Body.Bytes(), &response)
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: response.Id})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+}
+
+func TestGetWorkflowPaymentById(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldReturnPayment", func(t *testing.T) {
+ paymentId := "1"
+
+ wwwContext, rec, user, _ := setupPaymentRequestTest(http.MethodGet,
+ fmt.Sprintf("/api/admin/payments/%s", paymentId), "{}")
+
+ wwwContext.SetParamNames("paymentId")
+ wwwContext.SetParamValues(paymentId)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock}
+ www.SetSystem(system)
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: user.EthereumAddr})
+ if err != nil {
+ panic(err)
+ }
+
+ if assert.NoError(t, GetWorkflowPaymentById(wwwContext)) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+
+ var response = paymentResponse{}
+ err := json.Unmarshal(rec.Body.Bytes(), &response)
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: response.Id})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+
+ t.Run("ShouldNotReturnPayment", func(t *testing.T) {
+ paymentId := "2"
+
+ wwwContext, rec, user, userOwner := setupPaymentRequestTest(http.MethodGet,
+ fmt.Sprintf("/api/admin/payments/%s", paymentId), "{}")
+
+ wwwContext.SetParamNames("paymentId")
+ wwwContext.SetParamValues(paymentId)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock}
+ www.SetSystem(system)
+
+ //here pass "userOwner" instead of "user"
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: userOwner.EthereumAddr})
+ if err != nil {
+ panic(err)
+ }
+
+ if assert.NoError(t, GetWorkflowPaymentById(wwwContext)) {
+ assert.Equal(t, http.StatusBadRequest, rec.Code)
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: paymentId})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+}
+
+func TestGetWorkflowPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldReturnPayment", func(t *testing.T) {
+ paymentId := "3"
+ txHash := "0x3"
+ from := "0x4"
+ status := model.PaymentStatusConfirmed
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from, Status: status})
+ if err != nil {
+ panic(err)
+ }
+
+ payment, err := getWorkflowPayment(workflowPaymentsDB, txHash, from, status)
+
+ assert.Nil(t, err)
+ assert.Equal(t, paymentId, payment.ID)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+ t.Run("ShouldNotReturnPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x3"
+ from := "0x4"
+ status := model.PaymentStatusConfirmed
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: "0x5", Status: status})
+ if err != nil {
+ panic(err)
+ }
+
+ payment, err := getWorkflowPayment(workflowPaymentsDB, txHash, from, status)
+
+ assert.Equal(t, strm.ErrNotFound, err)
+ assert.Nil(t, payment)
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: paymentId})
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestUpdateWorkflowPaymentPending(t *testing.T) {
+
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldUpdatePayment", func(t *testing.T) {
+ paymentId := "3"
+ txHash := "0x3"
+ from := "0x4"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: from, Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, updateWorkflowPaymentPending(workflowPaymentsDB, paymentId, txHash, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, paymentId, payment.ID)
+ assert.Equal(t, model.PaymentStatusPending, payment.Status)
+ assert.Equal(t, txHash, payment.TxHash)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+ t.Run("ShouldReturnErrorOnUpdatePaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x4"
+ from := "0x5"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: from, Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, strm.ErrNotFound, updateWorkflowPaymentPending(workflowPaymentsDB, paymentId, txHash, "0x6"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, paymentId, payment.ID)
+ assert.Equal(t, model.PaymentStatusCreated, payment.Status)
+ assert.Equal(t, "", payment.TxHash)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestCancelWorkflowPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldCancelWorkflowPayment", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x4"
+ from := "0x5"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusCancelled, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotCancelWorkflowPaymentIfStatusIsNotPending", func(t *testing.T) {
+ paymentId := "5"
+ txHash := "0x5"
+ from := "0x6"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotCancelWorkflowPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "6"
+ txHash := "0x6"
+ from := "0x7"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, "0x8"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusCreated, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestRedeemPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldRedeemWorkflowPayment", func(t *testing.T) {
+ paymentId := "7"
+ txHash := "0x7"
+ from := "0x8"
+ workflowId := "01-02"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, RedeemPayment(workflowPaymentsDB, workflowId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusRedeemed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldRedeemNewerPaymentItemIfTwoAvailable", func(t *testing.T) {
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: "8first", TxHash: "0x8", From: "0x9",
+ Status: model.PaymentStatusConfirmed, WorkflowID: "01-03"})
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: "9second", TxHash: "0x9", From: "0x9",
+ Status: model.PaymentStatusConfirmed, WorkflowID: "01-03"})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, RedeemPayment(workflowPaymentsDB, "01-03", "0x9"))
+
+ firstPayment, err := workflowPaymentsDB.Get("8first")
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, firstPayment.Status)
+
+ secondPayment, err := workflowPaymentsDB.Get("9second")
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusRedeemed, secondPayment.Status)
+
+ err = workflowPaymentsDB.Remove(firstPayment)
+ if err != nil {
+ panic(err)
+ }
+ err = workflowPaymentsDB.Remove(secondPayment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotRedeemWorkflowPaymentIfStatusIsPending", func(t *testing.T) {
+ paymentId := "9"
+ txHash := "0x9"
+ from := "0x10"
+ workflowId := "01-03"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusPending, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, RedeemPayment(workflowPaymentsDB, workflowId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusPending, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotRedeemWorkflowPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "10"
+ txHash := "0x10"
+ from := "0x11"
+ workflowId := "01-04"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, RedeemPayment(workflowPaymentsDB, workflowId, "0x12"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestCheckIfWorkflowPaymentRequired(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldRequirePaymentIfWorkflowNotForFree", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.True(t, isPaymentRequired(false, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentIfWorkflowNotForFreeButAlreadyStarted", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(true, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentIfWorkflowIsFree", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 0, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(false, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentForWorkflowOwner", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(false, workflow, "1"))
+ })
+}
+
+var errCleanupTestData = errors.New("db data has not been cleanup up after finishing tests")
+
+func up(t *testing.T) (*gomock.Controller, storm.WorkflowPaymentsDBInterface) {
+ mockCtrl := gomock.NewController(t)
+
+ workflowPaymentsDB, err := storm.NewWorkflowPaymentDB(".test_data")
+ if err != nil {
+ panic(err)
+ }
+ return mockCtrl, workflowPaymentsDB
+}
+
+func down(mockCtrl *gomock.Controller, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) {
+
+ mockCtrl.Finish()
+
+ payments, err := workflowPaymentsDB.All()
+ if err != nil {
+ panic(err)
+ }
+ if len(payments) != 0 {
+ panic(errCleanupTestData)
+ }
+
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir, storm.WorkflowPaymentDB))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(".test_data")
+ if err != nil {
+ panic(err.Error())
+ }
+
+}
diff --git a/main/handlers/payment/handler_test.go-e b/main/handlers/payment/handler_test.go-e
new file mode 100644
index 000000000..de84dfb75
--- /dev/null
+++ b/main/handlers/payment/handler_test.go-e
@@ -0,0 +1,549 @@
+package payment
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+
+ strm "github.com/asdine/storm"
+
+ "git.proxeus.com/core/central/main/www"
+ "git.proxeus.com/core/central/sys"
+ "git.proxeus.com/core/central/sys/db/storm"
+ "git.proxeus.com/core/central/sys/model"
+
+ sysSess "git.proxeus.com/core/central/sys/session"
+)
+
+func setupPaymentRequestTest(httpMethod, targetUrl, body string) (*www.Context, *httptest.ResponseRecorder, *model.User, *model.User) {
+ e := echo.New()
+ req := httptest.NewRequest(httpMethod, targetUrl, strings.NewReader(body))
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ rec := httptest.NewRecorder()
+ c := e.NewContext(req, rec)
+
+ sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
+ c.Set("_session_store", sessionStore)
+ sysSession := &sysSess.Session{}
+ sysSession.SetUserID("1")
+
+ c.Set("sys.session", sysSession)
+ wwwContext := &www.Context{Context: c}
+ wwwContext.SetRequest(req)
+
+ user := &model.User{}
+ user.EthereumAddr = "0x00"
+
+ ownerUser := &model.User{}
+ ownerUser.EthereumAddr = "0x3"
+
+ return wwwContext, rec, user, ownerUser
+}
+
+type paymentResponse struct {
+ Id string `json:id`
+}
+
+func TestCreateWorkflowPayment(t *testing.T) {
+
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ body := `{"workflowId":"552a2f0e-c6c4-403b-8aaf-2d9ebf55eb8f"}`
+
+ wwwContext, rec, user, _ := setupPaymentRequestTest(http.MethodPost, "/api/admin/payments", body)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ workflow := &model.WorkflowItem{Price: 2000000000000000000}
+ workflow.Owner = user.EthereumAddr
+ workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
+ workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock, Workflow: workflowDBMock}
+ www.SetSystem(system)
+
+ t.Run("ShouldCreatePaymentItem", func(t *testing.T) {
+ if assert.NoError(t, CreateWorkflowPayment(wwwContext)) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+
+ var response = paymentResponse{}
+ err := json.Unmarshal(rec.Body.Bytes(), &response)
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: response.Id})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+}
+
+func TestGetWorkflowPaymentById(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldReturnPayment", func(t *testing.T) {
+ paymentId := "1"
+
+ wwwContext, rec, user, _ := setupPaymentRequestTest(http.MethodGet,
+ fmt.Sprintf("/api/admin/payments/%s", paymentId), "{}")
+
+ wwwContext.SetParamNames("paymentId")
+ wwwContext.SetParamValues(paymentId)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock}
+ www.SetSystem(system)
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: user.EthereumAddr})
+ if err != nil {
+ panic(err)
+ }
+
+ if assert.NoError(t, GetWorkflowPaymentById(wwwContext)) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+
+ var response = paymentResponse{}
+ err := json.Unmarshal(rec.Body.Bytes(), &response)
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: response.Id})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+
+ t.Run("ShouldNotReturnPayment", func(t *testing.T) {
+ paymentId := "2"
+
+ wwwContext, rec, user, userOwner := setupPaymentRequestTest(http.MethodGet,
+ fmt.Sprintf("/api/admin/payments/%s", paymentId), "{}")
+
+ wwwContext.SetParamNames("paymentId")
+ wwwContext.SetParamValues(paymentId)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock}
+ www.SetSystem(system)
+
+ //here pass "userOwner" instead of "user"
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: userOwner.EthereumAddr})
+ if err != nil {
+ panic(err)
+ }
+
+ if assert.NoError(t, GetWorkflowPaymentById(wwwContext)) {
+ assert.Equal(t, http.StatusBadRequest, rec.Code)
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: paymentId})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+}
+
+func TestGetWorkflowPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldReturnPayment", func(t *testing.T) {
+ paymentId := "3"
+ txHash := "0x3"
+ from := "0x4"
+ status := model.PaymentStatusConfirmed
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from, Status: status})
+ if err != nil {
+ panic(err)
+ }
+
+ payment, err := getWorkflowPayment(workflowPaymentsDB, txHash, from, status)
+
+ assert.Nil(t, err)
+ assert.Equal(t, paymentId, payment.ID)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+ t.Run("ShouldNotReturnPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x3"
+ from := "0x4"
+ status := model.PaymentStatusConfirmed
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: "0x5", Status: status})
+ if err != nil {
+ panic(err)
+ }
+
+ payment, err := getWorkflowPayment(workflowPaymentsDB, txHash, from, status)
+
+ assert.Equal(t, strm.ErrNotFound, err)
+ assert.Nil(t, payment)
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: paymentId})
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestUpdateWorkflowPaymentPending(t *testing.T) {
+
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldUpdatePayment", func(t *testing.T) {
+ paymentId := "3"
+ txHash := "0x3"
+ from := "0x4"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: from, Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, updateWorkflowPaymentPending(workflowPaymentsDB, paymentId, txHash, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, paymentId, payment.ID)
+ assert.Equal(t, model.PaymentStatusPending, payment.Status)
+ assert.Equal(t, txHash, payment.TxHash)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+ t.Run("ShouldReturnErrorOnUpdatePaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x4"
+ from := "0x5"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: from, Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, strm.ErrNotFound, updateWorkflowPaymentPending(workflowPaymentsDB, paymentId, txHash, "0x6"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, paymentId, payment.ID)
+ assert.Equal(t, model.PaymentStatusCreated, payment.Status)
+ assert.Equal(t, "", payment.TxHash)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestCancelWorkflowPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldCancelWorkflowPayment", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x4"
+ from := "0x5"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusCancelled, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotCancelWorkflowPaymentIfStatusIsNotPending", func(t *testing.T) {
+ paymentId := "5"
+ txHash := "0x5"
+ from := "0x6"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotCancelWorkflowPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "6"
+ txHash := "0x6"
+ from := "0x7"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, "0x8"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusCreated, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestRedeemPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldRedeemWorkflowPayment", func(t *testing.T) {
+ paymentId := "7"
+ txHash := "0x7"
+ from := "0x8"
+ workflowId := "01-02"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, RedeemPayment(workflowPaymentsDB, workflowId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusRedeemed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldRedeemNewerPaymentItemIfTwoAvailable", func(t *testing.T) {
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: "8first", TxHash: "0x8", From: "0x9",
+ Status: model.PaymentStatusConfirmed, WorkflowID: "01-03"})
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: "9second", TxHash: "0x9", From: "0x9",
+ Status: model.PaymentStatusConfirmed, WorkflowID: "01-03"})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, RedeemPayment(workflowPaymentsDB, "01-03", "0x9"))
+
+ firstPayment, err := workflowPaymentsDB.Get("8first")
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, firstPayment.Status)
+
+ secondPayment, err := workflowPaymentsDB.Get("9second")
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusRedeemed, secondPayment.Status)
+
+ err = workflowPaymentsDB.Remove(firstPayment)
+ if err != nil {
+ panic(err)
+ }
+ err = workflowPaymentsDB.Remove(secondPayment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotRedeemWorkflowPaymentIfStatusIsPending", func(t *testing.T) {
+ paymentId := "9"
+ txHash := "0x9"
+ from := "0x10"
+ workflowId := "01-03"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusPending, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, RedeemPayment(workflowPaymentsDB, workflowId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusPending, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotRedeemWorkflowPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "10"
+ txHash := "0x10"
+ from := "0x11"
+ workflowId := "01-04"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, RedeemPayment(workflowPaymentsDB, workflowId, "0x12"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestCheckIfWorkflowPaymentRequired(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldRequirePaymentIfWorkflowNotForFree", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.True(t, isPaymentRequired(false, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentIfWorkflowNotForFreeButAlreadyStarted", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(true, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentIfWorkflowIsFree", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 0, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(false, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentForWorkflowOwner", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(false, workflow, "1"))
+ })
+}
+
+var errCleanupTestData = errors.New("db data has not been cleanup up after finishing tests")
+
+func up(t *testing.T) (*gomock.Controller, storm.WorkflowPaymentsDBInterface) {
+ mockCtrl := gomock.NewController(t)
+
+ workflowPaymentsDB, err := storm.NewWorkflowPaymentDB(".test_data")
+ if err != nil {
+ panic(err)
+ }
+ return mockCtrl, workflowPaymentsDB
+}
+
+func down(mockCtrl *gomock.Controller, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) {
+
+ mockCtrl.Finish()
+
+ payments, err := workflowPaymentsDB.All()
+ if err != nil {
+ panic(err)
+ }
+ if len(payments) != 0 {
+ panic(errCleanupTestData)
+ }
+
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir, storm.WorkflowPaymentDB))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(".test_data")
+ if err != nil {
+ panic(err.Error())
+ }
+
+}
diff --git a/main/handlers/routes.go b/main/handlers/routes.go
index 5d9ac5906..14f01522c 100644
--- a/main/handlers/routes.go
+++ b/main/handlers/routes.go
@@ -3,6 +3,8 @@ package handlers
import (
"strings"
+ "git.proxeus.com/core/central/main/handlers/payment"
+
"git.proxeus.com/core/central/main/handlers/api"
"github.com/labstack/echo"
@@ -12,22 +14,14 @@ import (
"git.proxeus.com/core/central/main/handlers/template_ide"
"git.proxeus.com/core/central/main/handlers/workflow"
"git.proxeus.com/core/central/main/www"
- "git.proxeus.com/core/central/sys"
"git.proxeus.com/core/central/sys/model"
)
-func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
- configured, _ := system.Configured()
- var initialHandler *www.InitialHandler
- if !configured {
- initialHandler = www.NewInitialHandler(configured)
- e.Use(initialHandler.Handler)
- }
-
+func MainHostedAPI(e *echo.Echo, s *www.Security, version string) {
const (
- GET = echo.GET
- POST = echo.POST
- //PUT = echo.PUT
+ GET = echo.GET
+ POST = echo.POST
+ PUT = echo.PUT
DELETE = echo.DELETE
)
@@ -80,9 +74,9 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, USER, "/api/import", api.PostImport},
{GET, ROOT, "/api/init", api.GetInit},
{POST, ROOT, "/api/init", api.PostInit},
- {GET, PUBLIC, "/api/challenge", api.ChallengeHandler},
- {POST, PUBLIC, "/api/change/bcaddress", api.UpdateAddress},
- {POST, PUBLIC, "/api/change/email", api.ChangeEmailRequest},
+ {GET, PUBLIC, "/api/challenge", api.ChallengeHandler}, // Need session
+ {POST, PUBLIC, "/api/change/bcaddress", api.UpdateAddress}, // Need session
+ {POST, PUBLIC, "/api/change/email", api.ChangeEmailRequest}, // Need session
{POST, PUBLIC, "/api/change/email/:token", api.ChangeEmail},
{POST, PUBLIC, "/api/reset/password", api.ResetPasswordRequest},
{POST, PUBLIC, "/api/reset/password/:token", api.ResetPassword},
@@ -90,15 +84,19 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, PUBLIC, "/api/register/:token", api.Register},
{POST, PUBLIC, "/api/login", api.LoginHandler},
{POST, PUBLIC, "/api/logout", api.LogoutHandler},
- {GET, PUBLIC, "/api/config", api.ConfigHandler},
- {GET, PUBLIC, "/api/me", api.MeHandler},
+ {GET, PUBLIC, "/api/config", api.ConfigHandler(version)},
+ {GET, PUBLIC, "/api/me", api.MeHandler}, // Need session
{POST, USER, "/api/me", api.MeUpdateHandler},
+ // API Key session
+ {GET, PUBLIC, "/api/session/token", api.GetSessionTokenHandler},
+ {DELETE, USER, "/api/session/token", api.DeleteSessionTokenHandler},
+
{POST, USER, "/api/my/profile/photo", api.PutProfilePhotoHandler},
{GET, ROOT, "/api/settings/export", api.ExportSettings},
{GET, USER, "/api/userdata/export", api.ExportUserData},
- {GET, PUBLIC, "/api/document/:ID", api.DocumentHandler},
+ {GET, PUBLIC, "/api/document/:ID", api.DocumentHandler}, // Need session
{GET, PUBLIC, "/api/document/list", workflow.ListPublishedHandler},
{GET, PUBLIC, "/api/document/:ID/allAtOnce/schema", api.WorkflowSchema},
@@ -107,10 +105,10 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, GUEST, "/api/document/:ID/name", api.DocumentEditHandler},
{POST, GUEST, "/api/document/:ID/data", api.DocumentDataHandler},
{POST, GUEST, "/api/document/:ID/next", api.DocumentNextHandler},
- {GET, PUBLIC, "/api/document/:ID/prev", api.DocumentPrevHandler},
+ {GET, PUBLIC, "/api/document/:ID/prev", api.DocumentPrevHandler}, // Why PUBLIC access for prev when next is GUEST
{GET, GUEST, "/api/document/:ID/file/:inputName", api.DocumentFileGetHandler},
- {POST, PUBLIC, "/api/document/:ID/file/:inputName", api.DocumentFilePostHandler},
- {GET, PUBLIC, "/api/document/:ID/preview/:templateID/:lang/:format", api.DocumentPreviewHandler},
+ {POST, PUBLIC, "/api/document/:ID/file/:inputName", api.DocumentFilePostHandler}, // Should be GUEST
+ {GET, PUBLIC, "/api/document/:ID/preview/:templateID/:lang/:format", api.DocumentPreviewHandler}, // Should be GUEST
{GET, GUEST, "/api/document/:ID/delete", api.DocumentDeleteHandler},
{GET, GUEST, "/api/user/document", api.UserDocumentListHandler},
@@ -151,53 +149,58 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{GET, CREATOR, "/api/admin/workflow/:ID/delete", workflow.DeleteHandler},
{GET, USER, "/api/workflow/export", workflow.ExportWorkflow},
{GET, USER, "/api/user/workflow/list", workflow.ListPublishedHandler},
- {GET, PUBLIC, "/api/admin/workflow/list", workflow.ListHandler},
- {GET, PUBLIC, "/api/admin/workflow/:ID", workflow.GetHandler},
- {POST, PUBLIC, "/api/admin/workflow/update", workflow.UpdateHandler},
-
- {GET, PUBLIC, "/api/admin/workflow/:ID/payment", workflow.GetWorkflowPayment},
- {POST, PUBLIC, "/api/admin/workflow/:ID/payment/:txHash", workflow.AddWorkflowPayment},
-
- {GET, PUBLIC, "/api/management-list", api.ManagementListHandler},
+ {GET, PUBLIC, "/api/admin/workflow/list", workflow.ListHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/workflow/:ID", workflow.GetHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/workflow/update", workflow.UpdateHandler}, // Need session
+
+ // payment
+ {GET, USER, "/api/admin/payments/check", api.CheckForWorkflowPayment},
+ {POST, USER, "/api/admin/payments", payment.CreateWorkflowPayment},
+ {GET, USER, "/api/admin/payments/:paymentId", payment.GetWorkflowPaymentById},
+ {GET, USER, "/api/admin/payments", payment.GetWorkflowPayment},
+ {PUT, USER, "/api/admin/payments/:paymentId", payment.UpdateWorkflowPaymentPending},
+ {POST, USER, "/api/admin/payments/:paymentId/cancel", payment.CancelWorkflowPayment},
+ {GET, SUPERADMIN, "/api/admin/payments/list", payment.ListPayments},
+ {DELETE, SUPERADMIN, "/api/admin/payments/:paymentId", payment.DeleteWorkflowPayment},
// form builder
- {GET, PUBLIC, "/api/form/component", formbuilder.GetComponentsHandler},
+ {GET, PUBLIC, "/api/form/component", formbuilder.GetComponentsHandler}, // `Need session`
{GET, USER, "/api/form/export", formbuilder.ExportForms},
{GET, CREATOR, "/api/admin/form/:ID/delete", formbuilder.DeleteHandler},
- {GET, PUBLIC, "/api/admin/form/list", formbuilder.ListHandler},
+ {GET, PUBLIC, "/api/admin/form/list", formbuilder.ListHandler}, // Need session
{GET, USER, "/api/admin/:type/list", workflow.ListCustomNodeHandler},
- {GET, PUBLIC, "/api/admin/form/:formID", formbuilder.GetOneFormHandler},
- {POST, PUBLIC, "/api/admin/form/update", formbuilder.UpdateFormHandler},
+ {GET, PUBLIC, "/api/admin/form/:formID", formbuilder.GetOneFormHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/form/update", formbuilder.UpdateFormHandler}, // Need session
- {GET, PUBLIC, "/api/admin/form/component", formbuilder.GetComponentsHandler},
+ {GET, PUBLIC, "/api/admin/form/component", formbuilder.GetComponentsHandler}, // Need session
{POST, SUPERADMIN, "/api/admin/form/component", formbuilder.SetComponentHandler},
{DELETE, SUPERADMIN, "/api/admin/form/component/:id", formbuilder.DeleteComponentHandler},
- {GET, PUBLIC, "/api/admin/form/vars", formbuilder.VarsHandler},
+ {GET, PUBLIC, "/api/admin/form/vars", formbuilder.VarsHandler}, // Need session
- {POST, PUBLIC, "/api/admin/form/test/setFormSrc/:id", formbuilder.SetFormSrcHandler},
- {GET, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.GetDataId},
- {POST, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.TestFormDataHandler},
- {GET, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.GetFileIdFieldName},
+ {POST, PUBLIC, "/api/admin/form/test/setFormSrc/:id", formbuilder.SetFormSrcHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.GetDataId}, // Need session
+ {POST, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.TestFormDataHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.GetFileIdFieldName}, // Need session
{GET, PUBLIC, "/api/admin/form/file/types", formbuilder.GetFileTypes},
- {POST, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.PostFileIdFieldName},
+ {POST, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.PostFileIdFieldName}, // Need session
// template IDE
{GET, CREATOR, "/api/admin/template/:ID/delete", template_ide.DeleteHandler},
{GET, USER, "/api/template/export", template_ide.ExportTemplate},
- {GET, PUBLIC, "/api/admin/template/vars", template_ide.VarsTemplateHandler},
- {GET, PUBLIC, "/api/admin/template/list", template_ide.ListHandler},
- {POST, PUBLIC, "/api/admin/template/update", template_ide.UpdateHandler},
- {GET, PUBLIC, "/api/admin/template/:id", template_ide.OneTmplHandler},
- {GET, PUBLIC, "/api/admin/template/download/:id/:lang", template_ide.DownloadTemplateHandler},
- {POST, PUBLIC, "/api/admin/template/upload/:id/:lang", template_ide.UploadTemplateHandler},
- {GET, PUBLIC, "/api/admin/template/delete/:id/:lang", template_ide.DeleteTemplateHandler},
-
- {GET, PUBLIC, "/api/admin/template/ide/active/:id/:lang", template_ide.IdeSetActiveHandler},
- {POST, PUBLIC, "/api/admin/template/ide/upload/:id/:lang", template_ide.IdePostUploadHandler},
- {GET, PUBLIC, "/api/admin/template/ide/delete/:id/:lang", template_ide.IdeGetDeleteHandler},
- {GET, PUBLIC, "/api/admin/template/ide/download/:id", template_ide.IdeGetDownloadHandler},
+ {GET, PUBLIC, "/api/admin/template/vars", template_ide.VarsTemplateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/list", template_ide.ListHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/update", template_ide.UpdateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/:id", template_ide.OneTmplHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/download/:id/:lang", template_ide.DownloadTemplateHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/upload/:id/:lang", template_ide.UploadTemplateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/delete/:id/:lang", template_ide.DeleteTemplateHandler}, // Need session
+
+ {GET, PUBLIC, "/api/admin/template/ide/active/:id/:lang", template_ide.IdeSetActiveHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/ide/upload/:id/:lang", template_ide.IdePostUploadHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/ide/delete/:id/:lang", template_ide.IdeGetDeleteHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/ide/download/:id", template_ide.IdeGetDownloadHandler}, // Need session
{GET, CREATOR, "/api/admin/template/ide/tmplAssistanceDownload", template_ide.IdeGetTmpAssDownload},
- {GET, PUBLIC, "/api/admin/template/ide/form", template_ide.IdeFormHandler},
+ {GET, PUBLIC, "/api/admin/template/ide/form", template_ide.IdeFormHandler}, // Need session
}
addEndpoint := func(r r, ms ...echo.MiddlewareFunc) {
diff --git a/main/handlers/routes.go-e b/main/handlers/routes.go-e
index 5d9ac5906..14f01522c 100644
--- a/main/handlers/routes.go-e
+++ b/main/handlers/routes.go-e
@@ -3,6 +3,8 @@ package handlers
import (
"strings"
+ "git.proxeus.com/core/central/main/handlers/payment"
+
"git.proxeus.com/core/central/main/handlers/api"
"github.com/labstack/echo"
@@ -12,22 +14,14 @@ import (
"git.proxeus.com/core/central/main/handlers/template_ide"
"git.proxeus.com/core/central/main/handlers/workflow"
"git.proxeus.com/core/central/main/www"
- "git.proxeus.com/core/central/sys"
"git.proxeus.com/core/central/sys/model"
)
-func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
- configured, _ := system.Configured()
- var initialHandler *www.InitialHandler
- if !configured {
- initialHandler = www.NewInitialHandler(configured)
- e.Use(initialHandler.Handler)
- }
-
+func MainHostedAPI(e *echo.Echo, s *www.Security, version string) {
const (
- GET = echo.GET
- POST = echo.POST
- //PUT = echo.PUT
+ GET = echo.GET
+ POST = echo.POST
+ PUT = echo.PUT
DELETE = echo.DELETE
)
@@ -80,9 +74,9 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, USER, "/api/import", api.PostImport},
{GET, ROOT, "/api/init", api.GetInit},
{POST, ROOT, "/api/init", api.PostInit},
- {GET, PUBLIC, "/api/challenge", api.ChallengeHandler},
- {POST, PUBLIC, "/api/change/bcaddress", api.UpdateAddress},
- {POST, PUBLIC, "/api/change/email", api.ChangeEmailRequest},
+ {GET, PUBLIC, "/api/challenge", api.ChallengeHandler}, // Need session
+ {POST, PUBLIC, "/api/change/bcaddress", api.UpdateAddress}, // Need session
+ {POST, PUBLIC, "/api/change/email", api.ChangeEmailRequest}, // Need session
{POST, PUBLIC, "/api/change/email/:token", api.ChangeEmail},
{POST, PUBLIC, "/api/reset/password", api.ResetPasswordRequest},
{POST, PUBLIC, "/api/reset/password/:token", api.ResetPassword},
@@ -90,15 +84,19 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, PUBLIC, "/api/register/:token", api.Register},
{POST, PUBLIC, "/api/login", api.LoginHandler},
{POST, PUBLIC, "/api/logout", api.LogoutHandler},
- {GET, PUBLIC, "/api/config", api.ConfigHandler},
- {GET, PUBLIC, "/api/me", api.MeHandler},
+ {GET, PUBLIC, "/api/config", api.ConfigHandler(version)},
+ {GET, PUBLIC, "/api/me", api.MeHandler}, // Need session
{POST, USER, "/api/me", api.MeUpdateHandler},
+ // API Key session
+ {GET, PUBLIC, "/api/session/token", api.GetSessionTokenHandler},
+ {DELETE, USER, "/api/session/token", api.DeleteSessionTokenHandler},
+
{POST, USER, "/api/my/profile/photo", api.PutProfilePhotoHandler},
{GET, ROOT, "/api/settings/export", api.ExportSettings},
{GET, USER, "/api/userdata/export", api.ExportUserData},
- {GET, PUBLIC, "/api/document/:ID", api.DocumentHandler},
+ {GET, PUBLIC, "/api/document/:ID", api.DocumentHandler}, // Need session
{GET, PUBLIC, "/api/document/list", workflow.ListPublishedHandler},
{GET, PUBLIC, "/api/document/:ID/allAtOnce/schema", api.WorkflowSchema},
@@ -107,10 +105,10 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, GUEST, "/api/document/:ID/name", api.DocumentEditHandler},
{POST, GUEST, "/api/document/:ID/data", api.DocumentDataHandler},
{POST, GUEST, "/api/document/:ID/next", api.DocumentNextHandler},
- {GET, PUBLIC, "/api/document/:ID/prev", api.DocumentPrevHandler},
+ {GET, PUBLIC, "/api/document/:ID/prev", api.DocumentPrevHandler}, // Why PUBLIC access for prev when next is GUEST
{GET, GUEST, "/api/document/:ID/file/:inputName", api.DocumentFileGetHandler},
- {POST, PUBLIC, "/api/document/:ID/file/:inputName", api.DocumentFilePostHandler},
- {GET, PUBLIC, "/api/document/:ID/preview/:templateID/:lang/:format", api.DocumentPreviewHandler},
+ {POST, PUBLIC, "/api/document/:ID/file/:inputName", api.DocumentFilePostHandler}, // Should be GUEST
+ {GET, PUBLIC, "/api/document/:ID/preview/:templateID/:lang/:format", api.DocumentPreviewHandler}, // Should be GUEST
{GET, GUEST, "/api/document/:ID/delete", api.DocumentDeleteHandler},
{GET, GUEST, "/api/user/document", api.UserDocumentListHandler},
@@ -151,53 +149,58 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{GET, CREATOR, "/api/admin/workflow/:ID/delete", workflow.DeleteHandler},
{GET, USER, "/api/workflow/export", workflow.ExportWorkflow},
{GET, USER, "/api/user/workflow/list", workflow.ListPublishedHandler},
- {GET, PUBLIC, "/api/admin/workflow/list", workflow.ListHandler},
- {GET, PUBLIC, "/api/admin/workflow/:ID", workflow.GetHandler},
- {POST, PUBLIC, "/api/admin/workflow/update", workflow.UpdateHandler},
-
- {GET, PUBLIC, "/api/admin/workflow/:ID/payment", workflow.GetWorkflowPayment},
- {POST, PUBLIC, "/api/admin/workflow/:ID/payment/:txHash", workflow.AddWorkflowPayment},
-
- {GET, PUBLIC, "/api/management-list", api.ManagementListHandler},
+ {GET, PUBLIC, "/api/admin/workflow/list", workflow.ListHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/workflow/:ID", workflow.GetHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/workflow/update", workflow.UpdateHandler}, // Need session
+
+ // payment
+ {GET, USER, "/api/admin/payments/check", api.CheckForWorkflowPayment},
+ {POST, USER, "/api/admin/payments", payment.CreateWorkflowPayment},
+ {GET, USER, "/api/admin/payments/:paymentId", payment.GetWorkflowPaymentById},
+ {GET, USER, "/api/admin/payments", payment.GetWorkflowPayment},
+ {PUT, USER, "/api/admin/payments/:paymentId", payment.UpdateWorkflowPaymentPending},
+ {POST, USER, "/api/admin/payments/:paymentId/cancel", payment.CancelWorkflowPayment},
+ {GET, SUPERADMIN, "/api/admin/payments/list", payment.ListPayments},
+ {DELETE, SUPERADMIN, "/api/admin/payments/:paymentId", payment.DeleteWorkflowPayment},
// form builder
- {GET, PUBLIC, "/api/form/component", formbuilder.GetComponentsHandler},
+ {GET, PUBLIC, "/api/form/component", formbuilder.GetComponentsHandler}, // `Need session`
{GET, USER, "/api/form/export", formbuilder.ExportForms},
{GET, CREATOR, "/api/admin/form/:ID/delete", formbuilder.DeleteHandler},
- {GET, PUBLIC, "/api/admin/form/list", formbuilder.ListHandler},
+ {GET, PUBLIC, "/api/admin/form/list", formbuilder.ListHandler}, // Need session
{GET, USER, "/api/admin/:type/list", workflow.ListCustomNodeHandler},
- {GET, PUBLIC, "/api/admin/form/:formID", formbuilder.GetOneFormHandler},
- {POST, PUBLIC, "/api/admin/form/update", formbuilder.UpdateFormHandler},
+ {GET, PUBLIC, "/api/admin/form/:formID", formbuilder.GetOneFormHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/form/update", formbuilder.UpdateFormHandler}, // Need session
- {GET, PUBLIC, "/api/admin/form/component", formbuilder.GetComponentsHandler},
+ {GET, PUBLIC, "/api/admin/form/component", formbuilder.GetComponentsHandler}, // Need session
{POST, SUPERADMIN, "/api/admin/form/component", formbuilder.SetComponentHandler},
{DELETE, SUPERADMIN, "/api/admin/form/component/:id", formbuilder.DeleteComponentHandler},
- {GET, PUBLIC, "/api/admin/form/vars", formbuilder.VarsHandler},
+ {GET, PUBLIC, "/api/admin/form/vars", formbuilder.VarsHandler}, // Need session
- {POST, PUBLIC, "/api/admin/form/test/setFormSrc/:id", formbuilder.SetFormSrcHandler},
- {GET, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.GetDataId},
- {POST, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.TestFormDataHandler},
- {GET, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.GetFileIdFieldName},
+ {POST, PUBLIC, "/api/admin/form/test/setFormSrc/:id", formbuilder.SetFormSrcHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.GetDataId}, // Need session
+ {POST, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.TestFormDataHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.GetFileIdFieldName}, // Need session
{GET, PUBLIC, "/api/admin/form/file/types", formbuilder.GetFileTypes},
- {POST, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.PostFileIdFieldName},
+ {POST, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.PostFileIdFieldName}, // Need session
// template IDE
{GET, CREATOR, "/api/admin/template/:ID/delete", template_ide.DeleteHandler},
{GET, USER, "/api/template/export", template_ide.ExportTemplate},
- {GET, PUBLIC, "/api/admin/template/vars", template_ide.VarsTemplateHandler},
- {GET, PUBLIC, "/api/admin/template/list", template_ide.ListHandler},
- {POST, PUBLIC, "/api/admin/template/update", template_ide.UpdateHandler},
- {GET, PUBLIC, "/api/admin/template/:id", template_ide.OneTmplHandler},
- {GET, PUBLIC, "/api/admin/template/download/:id/:lang", template_ide.DownloadTemplateHandler},
- {POST, PUBLIC, "/api/admin/template/upload/:id/:lang", template_ide.UploadTemplateHandler},
- {GET, PUBLIC, "/api/admin/template/delete/:id/:lang", template_ide.DeleteTemplateHandler},
-
- {GET, PUBLIC, "/api/admin/template/ide/active/:id/:lang", template_ide.IdeSetActiveHandler},
- {POST, PUBLIC, "/api/admin/template/ide/upload/:id/:lang", template_ide.IdePostUploadHandler},
- {GET, PUBLIC, "/api/admin/template/ide/delete/:id/:lang", template_ide.IdeGetDeleteHandler},
- {GET, PUBLIC, "/api/admin/template/ide/download/:id", template_ide.IdeGetDownloadHandler},
+ {GET, PUBLIC, "/api/admin/template/vars", template_ide.VarsTemplateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/list", template_ide.ListHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/update", template_ide.UpdateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/:id", template_ide.OneTmplHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/download/:id/:lang", template_ide.DownloadTemplateHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/upload/:id/:lang", template_ide.UploadTemplateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/delete/:id/:lang", template_ide.DeleteTemplateHandler}, // Need session
+
+ {GET, PUBLIC, "/api/admin/template/ide/active/:id/:lang", template_ide.IdeSetActiveHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/ide/upload/:id/:lang", template_ide.IdePostUploadHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/ide/delete/:id/:lang", template_ide.IdeGetDeleteHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/ide/download/:id", template_ide.IdeGetDownloadHandler}, // Need session
{GET, CREATOR, "/api/admin/template/ide/tmplAssistanceDownload", template_ide.IdeGetTmpAssDownload},
- {GET, PUBLIC, "/api/admin/template/ide/form", template_ide.IdeFormHandler},
+ {GET, PUBLIC, "/api/admin/template/ide/form", template_ide.IdeFormHandler}, // Need session
}
addEndpoint := func(r r, ms ...echo.MiddlewareFunc) {
diff --git a/main/handlers/workflow/handlers.go b/main/handlers/workflow/handlers.go
index 35943ffce..5a0b1c504 100644
--- a/main/handlers/workflow/handlers.go
+++ b/main/handlers/workflow/handlers.go
@@ -3,9 +3,6 @@ package workflow
import (
"log"
"net/http"
- "strings"
-
- "github.com/pkg/errors"
"git.proxeus.com/core/central/sys/workflow"
@@ -158,120 +155,6 @@ func UpdateHandler(e echo.Context) error {
return c.NoContent(http.StatusBadRequest)
}
-// Checks if a workflow payment exist in the db.
-// The payment can be retrieved either by txHash or workflowId/documentId and ethereum address.
-// Getting the payment with txHash is used when metamask notifies the frontend that a payment has been received but the backend has not yet been notified what
-// workflow the payment was for. The backend verifies if it has received the payment.
-// Once the payment process is finished and the backend has been notified what workflow the payment is for, the payment is checked/retrieved in
-// workflowId/documentId and ethereum address.
-func GetWorkflowPayment(e echo.Context) error {
- c := e.(*www.Context)
- txHash := c.QueryParam("txHash")
- workflowId := c.Param("ID")
-
- var (
- workflowPaymentItem *model.WorkflowPaymentItem
- err error
- )
- if txHash == "" {
- sess := c.Session(false)
- user, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
- workflowPaymentItem, err = c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowId, user.EthereumAddr)
- if err != nil {
- if err.Error() == "not found" {
- return c.NoContent(http.StatusNotFound)
- }
- return c.NoContent(http.StatusBadRequest)
- }
- err = checkPayment(c, workflowId, workflowPaymentItem)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
- } else {
- workflowPaymentItem, err = c.System().DB.WorkflowPaymentsDB.GetByTxHash(txHash)
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
- }
-
- log.Println("[workflowHandler][GetWorkflowPayment]", workflowPaymentItem.TxHash)
-
- return c.JSON(http.StatusOK, workflowPaymentItem)
-}
-
-// Once the payment has been confirmed this function redeems the payment for a worklflowId.
-// If all parameters in checkPayment function are valid the worfklowId is set to the workflowPaymentItem.
-func AddWorkflowPayment(e echo.Context) error {
- c := e.(*www.Context)
- txHash := c.Param("txHash")
- workflowId := c.Param("ID")
-
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByTxHash(txHash)
- if err != nil || workflowPaymentItem.WorkflowID != "" {
- return c.NoContent(http.StatusBadRequest)
- }
-
- err = checkPayment(c, workflowId, workflowPaymentItem)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
-
- workflowPaymentItem.WorkflowID = workflowId
-
- err = c.System().DB.WorkflowPaymentsDB.Add(workflowPaymentItem)
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
-
- return c.NoContent(http.StatusOK)
-}
-
-var errPaymentFailed = errors.New("failed to validate payment")
-
-// Verify that a payment can be claimed by user by validating payment parameter against workflow parameters.
-// A payment can only be claimed if all these parameters match: price, payer, receiver
-func checkPayment(c *www.Context, workflowId string, workflowPaymentItem *model.WorkflowPaymentItem) error {
- sess := c.Session(false)
- if sess == nil {
- return errPaymentFailed
- }
- workflow, err := c.System().DB.Workflow.Get(sess, workflowId)
- if err != nil {
- return err
- }
-
- if workflowPaymentItem.Xes != workflow.Price {
- return errPaymentFailed
- }
-
- payer, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil || payer == nil {
- return errPaymentFailed
- }
-
- if payer.EthereumAddr == "" {
- return errPaymentFailed
- }
-
- if !strings.EqualFold(workflowPaymentItem.From, payer.EthereumAddr) {
- return errPaymentFailed
- }
-
- workflowOwner, err := c.System().DB.User.Get(sess, workflow.Owner)
- if err != nil {
- return errPaymentFailed
- }
-
- if !strings.EqualFold(workflowPaymentItem.To, workflowOwner.EthereumAddr) {
- return errPaymentFailed
- }
-
- return nil
-}
-
func DeleteHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -295,17 +178,19 @@ func ListHandler(e echo.Context) error {
}
func listHandler(c *www.Context, publishedOnly bool) error {
- contains := c.QueryParam("c")
- a, err := c.Auth()
- if err != nil {
- return c.NoContent(http.StatusUnauthorized)
+ var sess model.Authorization
+ if s := c.Session(false); s != nil {
+ sess = s
}
+ contains := c.QueryParam("c")
settings := helpers.ReadReqSettings(c)
var dat []*model.WorkflowItem
+ var err error
+
if publishedOnly {
- dat, err = c.System().DB.Workflow.ListPublished(a, contains, settings)
+ dat, err = c.System().DB.Workflow.ListPublished(sess, contains, settings)
} else {
- dat, err = c.System().DB.Workflow.List(a, contains, settings)
+ dat, err = c.System().DB.Workflow.List(sess, contains, settings)
}
if err != nil {
diff --git a/main/handlers/workflow/handlers.go-e b/main/handlers/workflow/handlers.go-e
index 35943ffce..5a0b1c504 100644
--- a/main/handlers/workflow/handlers.go-e
+++ b/main/handlers/workflow/handlers.go-e
@@ -3,9 +3,6 @@ package workflow
import (
"log"
"net/http"
- "strings"
-
- "github.com/pkg/errors"
"git.proxeus.com/core/central/sys/workflow"
@@ -158,120 +155,6 @@ func UpdateHandler(e echo.Context) error {
return c.NoContent(http.StatusBadRequest)
}
-// Checks if a workflow payment exist in the db.
-// The payment can be retrieved either by txHash or workflowId/documentId and ethereum address.
-// Getting the payment with txHash is used when metamask notifies the frontend that a payment has been received but the backend has not yet been notified what
-// workflow the payment was for. The backend verifies if it has received the payment.
-// Once the payment process is finished and the backend has been notified what workflow the payment is for, the payment is checked/retrieved in
-// workflowId/documentId and ethereum address.
-func GetWorkflowPayment(e echo.Context) error {
- c := e.(*www.Context)
- txHash := c.QueryParam("txHash")
- workflowId := c.Param("ID")
-
- var (
- workflowPaymentItem *model.WorkflowPaymentItem
- err error
- )
- if txHash == "" {
- sess := c.Session(false)
- user, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
- workflowPaymentItem, err = c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowId, user.EthereumAddr)
- if err != nil {
- if err.Error() == "not found" {
- return c.NoContent(http.StatusNotFound)
- }
- return c.NoContent(http.StatusBadRequest)
- }
- err = checkPayment(c, workflowId, workflowPaymentItem)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
- } else {
- workflowPaymentItem, err = c.System().DB.WorkflowPaymentsDB.GetByTxHash(txHash)
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
- }
-
- log.Println("[workflowHandler][GetWorkflowPayment]", workflowPaymentItem.TxHash)
-
- return c.JSON(http.StatusOK, workflowPaymentItem)
-}
-
-// Once the payment has been confirmed this function redeems the payment for a worklflowId.
-// If all parameters in checkPayment function are valid the worfklowId is set to the workflowPaymentItem.
-func AddWorkflowPayment(e echo.Context) error {
- c := e.(*www.Context)
- txHash := c.Param("txHash")
- workflowId := c.Param("ID")
-
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByTxHash(txHash)
- if err != nil || workflowPaymentItem.WorkflowID != "" {
- return c.NoContent(http.StatusBadRequest)
- }
-
- err = checkPayment(c, workflowId, workflowPaymentItem)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
-
- workflowPaymentItem.WorkflowID = workflowId
-
- err = c.System().DB.WorkflowPaymentsDB.Add(workflowPaymentItem)
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
-
- return c.NoContent(http.StatusOK)
-}
-
-var errPaymentFailed = errors.New("failed to validate payment")
-
-// Verify that a payment can be claimed by user by validating payment parameter against workflow parameters.
-// A payment can only be claimed if all these parameters match: price, payer, receiver
-func checkPayment(c *www.Context, workflowId string, workflowPaymentItem *model.WorkflowPaymentItem) error {
- sess := c.Session(false)
- if sess == nil {
- return errPaymentFailed
- }
- workflow, err := c.System().DB.Workflow.Get(sess, workflowId)
- if err != nil {
- return err
- }
-
- if workflowPaymentItem.Xes != workflow.Price {
- return errPaymentFailed
- }
-
- payer, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil || payer == nil {
- return errPaymentFailed
- }
-
- if payer.EthereumAddr == "" {
- return errPaymentFailed
- }
-
- if !strings.EqualFold(workflowPaymentItem.From, payer.EthereumAddr) {
- return errPaymentFailed
- }
-
- workflowOwner, err := c.System().DB.User.Get(sess, workflow.Owner)
- if err != nil {
- return errPaymentFailed
- }
-
- if !strings.EqualFold(workflowPaymentItem.To, workflowOwner.EthereumAddr) {
- return errPaymentFailed
- }
-
- return nil
-}
-
func DeleteHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -295,17 +178,19 @@ func ListHandler(e echo.Context) error {
}
func listHandler(c *www.Context, publishedOnly bool) error {
- contains := c.QueryParam("c")
- a, err := c.Auth()
- if err != nil {
- return c.NoContent(http.StatusUnauthorized)
+ var sess model.Authorization
+ if s := c.Session(false); s != nil {
+ sess = s
}
+ contains := c.QueryParam("c")
settings := helpers.ReadReqSettings(c)
var dat []*model.WorkflowItem
+ var err error
+
if publishedOnly {
- dat, err = c.System().DB.Workflow.ListPublished(a, contains, settings)
+ dat, err = c.System().DB.Workflow.ListPublished(sess, contains, settings)
} else {
- dat, err = c.System().DB.Workflow.List(a, contains, settings)
+ dat, err = c.System().DB.Workflow.List(sess, contains, settings)
}
if err != nil {
diff --git a/main/handlers/workflow/handlers_test.go b/main/handlers/workflow/handlers_test.go
deleted file mode 100644
index 3be3be65a..000000000
--- a/main/handlers/workflow/handlers_test.go
+++ /dev/null
@@ -1,228 +0,0 @@
-package workflow
-
-import (
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/golang/mock/gomock"
-
- "git.proxeus.com/core/central/sys"
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-
- "github.com/gorilla/sessions"
- "github.com/labstack/echo"
- "github.com/stretchr/testify/assert"
-
- "git.proxeus.com/core/central/main/www"
- sysSess "git.proxeus.com/core/central/sys/session"
-)
-
-func setupPaymentTest(httpMethod, targetUrl string) (*www.Context, *httptest.ResponseRecorder, *model.User, *model.User) {
- e := echo.New()
- req := httptest.NewRequest(httpMethod, targetUrl, nil)
- rec := httptest.NewRecorder()
- c := e.NewContext(req, rec)
-
- sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
- c.Set("_session_store", sessionStore)
- sysSession := &sysSess.Session{}
- sysSession.SetUserID("1")
-
- c.Set("sys.session", sysSession)
- wwwContext := &www.Context{Context: c}
- wwwContext.SetRequest(req)
-
- user := &model.User{}
- user.EthereumAddr = "0x00"
-
- ownerUser := &model.User{}
- ownerUser.EthereumAddr = "0x3"
-
- return wwwContext, rec, user, ownerUser
-}
-
-func TestAddWorkflowPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("AddWorkflowPaymentShouldSucceed", func(t *testing.T) {
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
- workflowPaymentsDBMock.EXPECT().Add(gomock.Any()).Return(nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- }
- })
-
- t.Run("AddWorkflowPaymentShouldFailIncorrectPayer", func(t *testing.T) {
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0xWrong", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-
- t.Run("AddWorkflowPaymentShouldFailPaymentItemWorkflowIDAlreadySet", func(t *testing.T) {
- wwwContext, rec, user, _ := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- }
- })
-}
-
-func TestGetWorkflowPayment(t *testing.T) {
-
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("GetWorkflowPaymentByWorkflowIdAndFromEthAddressShouldSucceed", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(2)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- responseBody := rec.Body.String()
- successResponseJSON := `{"hash":"0x5","workflowID":"3","From":"0x00","To":"0x3","xes":2000000000000000000,"Status":"","createdAt":"0001-01-01T00:00:00Z"}`
- assert.Equal(t, successResponseJSON, strings.Trim(responseBody, "\n"))
- }
- })
-
- t.Run("GetWorkflowPaymentByTxHashShouldSucceed", func(t *testing.T) {
-
- wwwContext, rec, user, _ := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment?txHash=0x2222")
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Eq("0x2222")).Return(workflowPaymentItem, nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- responseBody := rec.Body.String()
- successResponseJSON := `{"hash":"0x5","workflowID":"3","From":"0x00","To":"0x3","xes":2000000000000000000,"Status":"","createdAt":"0001-01-01T00:00:00Z"}`
- assert.Equal(t, successResponseJSON, strings.Trim(responseBody, "\n"))
- }
- })
-
- t.Run("GetWorkflowPaymentShouldFailIncorrectPaymentAmount", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2100000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 1900000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-
- t.Run("GetWorkflowPaymentShouldFailIncorrectPayee", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(2)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2100000000000000000, From: user.EthereumAddr, To: "0xWrong", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2100000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-}
diff --git a/main/handlers/workflow/handlers_test.go-e b/main/handlers/workflow/handlers_test.go-e
deleted file mode 100644
index 3be3be65a..000000000
--- a/main/handlers/workflow/handlers_test.go-e
+++ /dev/null
@@ -1,228 +0,0 @@
-package workflow
-
-import (
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/golang/mock/gomock"
-
- "git.proxeus.com/core/central/sys"
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-
- "github.com/gorilla/sessions"
- "github.com/labstack/echo"
- "github.com/stretchr/testify/assert"
-
- "git.proxeus.com/core/central/main/www"
- sysSess "git.proxeus.com/core/central/sys/session"
-)
-
-func setupPaymentTest(httpMethod, targetUrl string) (*www.Context, *httptest.ResponseRecorder, *model.User, *model.User) {
- e := echo.New()
- req := httptest.NewRequest(httpMethod, targetUrl, nil)
- rec := httptest.NewRecorder()
- c := e.NewContext(req, rec)
-
- sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
- c.Set("_session_store", sessionStore)
- sysSession := &sysSess.Session{}
- sysSession.SetUserID("1")
-
- c.Set("sys.session", sysSession)
- wwwContext := &www.Context{Context: c}
- wwwContext.SetRequest(req)
-
- user := &model.User{}
- user.EthereumAddr = "0x00"
-
- ownerUser := &model.User{}
- ownerUser.EthereumAddr = "0x3"
-
- return wwwContext, rec, user, ownerUser
-}
-
-func TestAddWorkflowPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("AddWorkflowPaymentShouldSucceed", func(t *testing.T) {
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
- workflowPaymentsDBMock.EXPECT().Add(gomock.Any()).Return(nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- }
- })
-
- t.Run("AddWorkflowPaymentShouldFailIncorrectPayer", func(t *testing.T) {
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0xWrong", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-
- t.Run("AddWorkflowPaymentShouldFailPaymentItemWorkflowIDAlreadySet", func(t *testing.T) {
- wwwContext, rec, user, _ := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- }
- })
-}
-
-func TestGetWorkflowPayment(t *testing.T) {
-
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("GetWorkflowPaymentByWorkflowIdAndFromEthAddressShouldSucceed", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(2)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- responseBody := rec.Body.String()
- successResponseJSON := `{"hash":"0x5","workflowID":"3","From":"0x00","To":"0x3","xes":2000000000000000000,"Status":"","createdAt":"0001-01-01T00:00:00Z"}`
- assert.Equal(t, successResponseJSON, strings.Trim(responseBody, "\n"))
- }
- })
-
- t.Run("GetWorkflowPaymentByTxHashShouldSucceed", func(t *testing.T) {
-
- wwwContext, rec, user, _ := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment?txHash=0x2222")
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Eq("0x2222")).Return(workflowPaymentItem, nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- responseBody := rec.Body.String()
- successResponseJSON := `{"hash":"0x5","workflowID":"3","From":"0x00","To":"0x3","xes":2000000000000000000,"Status":"","createdAt":"0001-01-01T00:00:00Z"}`
- assert.Equal(t, successResponseJSON, strings.Trim(responseBody, "\n"))
- }
- })
-
- t.Run("GetWorkflowPaymentShouldFailIncorrectPaymentAmount", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2100000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 1900000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-
- t.Run("GetWorkflowPaymentShouldFailIncorrectPayee", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(2)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2100000000000000000, From: user.EthereumAddr, To: "0xWrong", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2100000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-}
diff --git a/main/main.go b/main/main.go
index abd56c13e..6474f4c56 100644
--- a/main/main.go
+++ b/main/main.go
@@ -2,19 +2,17 @@ package main
import (
"fmt"
- "log"
"net/http"
_ "net/http/pprof"
+ "os"
"path"
"github.com/labstack/echo"
- "github.com/labstack/echo/middleware"
"strings"
cfg "git.proxeus.com/core/central/main/config"
"git.proxeus.com/core/central/main/handlers"
- "git.proxeus.com/core/central/main/handlers/api"
"git.proxeus.com/core/central/main/handlers/assets"
"git.proxeus.com/core/central/main/www"
"git.proxeus.com/core/central/sys"
@@ -25,68 +23,26 @@ import (
// ServerVersion is added to http headers and can be set during making a build
var ServerVersion = "build-unknown"
-func xVersionHeader(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- c.Response().Header().Set("X-Version", ServerVersion)
- return next(c)
- }
-}
-
var embedded *www.Embedded
func main() {
- e := echo.New()
- //Simple Request Logging
- //e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
- // Format: "[echo] ${time_rfc3339} client=${remote_ip}, method=${method}, uri=${uri}, status=${status}\n",
- //}))
-
- //Request Logging with User Info and Body on Error
- e.Use(middleware.BodyDump(func(e echo.Context, reqBody, resBody []byte) {
- c := www.Context{Context: e}
- //c := e.(*www.Context)
- s := c.Session(false)
- if s == nil {
- return
- }
- if s.ID() != "" {
- id := s.UserID()
- user, err := c.System().DB.User.Get(s, id)
- if err != nil {
- return
- }
- userName := user.Name
- userAddr := user.EthereumAddr
- log.Println("[echo] Method: "+e.Request().Method, "Status:", e.Response().Status, "User: "+userAddr, "("+userName+")", "URI: "+e.Request().RequestURI)
- if len(reqBody) > 0 && c.Response().Status != 200 && c.Response().Status != 404 {
- fmt.Printf("[echo][errorrequest] %s\n", reqBody)
- }
- }
-
- }))
- e.HTTPErrorHandler = www.DefaultHTTPErrorHandler
- e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- return h(&www.Context{Context: c})
- }
- })
- e.Pre(xVersionHeader)
- c := middleware.DefaultSecureConfig
- c.XFrameOptions = ""
- e.Pre(middleware.SecureWithConfig(c))
-
- e.GET("/static/*", StaticHandler)
-
- www.SetupSession(e)
system, err := sys.NewWithSettings(cfg.Config.Settings)
if err != nil {
panic(err)
}
+
+ if system.TestMode {
+ fmt.Println("#######################################################")
+ fmt.Println("# STARTING PROXEUS IN TEST MODE - NOT FOR PRODUCTION #")
+ fmt.Println("#######################################################")
+ }
+
+ www.SetSystem(system)
+
embedded = &www.Embedded{Asset: assets.Asset}
sys.ReadAllFile = func(path string) ([]byte, error) {
return embedded.Asset(path)
}
- www.SetSystem(system)
go func() { //parse i18n from the UI assets to provide them under the translation section
i18nUIParser := i18n.NewUIParser()
@@ -122,14 +78,22 @@ func main() {
}
}()
- secure := www.NewSecurity()
+ e := www.Setup(ServerVersion)
- // Routes
- e.Pre(middleware.Secure())
+ // Static route
+ e.GET("/static/*", StaticHandler)
- api.ServerVersion = ServerVersion
+ // Initial config middleware
+ configured, err := system.Configured()
+ if err != nil && !os.IsNotExist(err) {
+ panic(err)
+ }
+ if !configured {
+ e.Use(www.NewInitialHandler(configured).Handler)
+ }
- handlers.MainHostedAPI(e, secure, system)
+ // Main routes
+ handlers.MainHostedAPI(e, www.NewSecurity(), ServerVersion)
www.StartServer(e, cfg.Config.ServiceAddress, false)
system.Shutdown()
diff --git a/main/main.go-e b/main/main.go-e
index abd56c13e..6474f4c56 100644
--- a/main/main.go-e
+++ b/main/main.go-e
@@ -2,19 +2,17 @@ package main
import (
"fmt"
- "log"
"net/http"
_ "net/http/pprof"
+ "os"
"path"
"github.com/labstack/echo"
- "github.com/labstack/echo/middleware"
"strings"
cfg "git.proxeus.com/core/central/main/config"
"git.proxeus.com/core/central/main/handlers"
- "git.proxeus.com/core/central/main/handlers/api"
"git.proxeus.com/core/central/main/handlers/assets"
"git.proxeus.com/core/central/main/www"
"git.proxeus.com/core/central/sys"
@@ -25,68 +23,26 @@ import (
// ServerVersion is added to http headers and can be set during making a build
var ServerVersion = "build-unknown"
-func xVersionHeader(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- c.Response().Header().Set("X-Version", ServerVersion)
- return next(c)
- }
-}
-
var embedded *www.Embedded
func main() {
- e := echo.New()
- //Simple Request Logging
- //e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
- // Format: "[echo] ${time_rfc3339} client=${remote_ip}, method=${method}, uri=${uri}, status=${status}\n",
- //}))
-
- //Request Logging with User Info and Body on Error
- e.Use(middleware.BodyDump(func(e echo.Context, reqBody, resBody []byte) {
- c := www.Context{Context: e}
- //c := e.(*www.Context)
- s := c.Session(false)
- if s == nil {
- return
- }
- if s.ID() != "" {
- id := s.UserID()
- user, err := c.System().DB.User.Get(s, id)
- if err != nil {
- return
- }
- userName := user.Name
- userAddr := user.EthereumAddr
- log.Println("[echo] Method: "+e.Request().Method, "Status:", e.Response().Status, "User: "+userAddr, "("+userName+")", "URI: "+e.Request().RequestURI)
- if len(reqBody) > 0 && c.Response().Status != 200 && c.Response().Status != 404 {
- fmt.Printf("[echo][errorrequest] %s\n", reqBody)
- }
- }
-
- }))
- e.HTTPErrorHandler = www.DefaultHTTPErrorHandler
- e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- return h(&www.Context{Context: c})
- }
- })
- e.Pre(xVersionHeader)
- c := middleware.DefaultSecureConfig
- c.XFrameOptions = ""
- e.Pre(middleware.SecureWithConfig(c))
-
- e.GET("/static/*", StaticHandler)
-
- www.SetupSession(e)
system, err := sys.NewWithSettings(cfg.Config.Settings)
if err != nil {
panic(err)
}
+
+ if system.TestMode {
+ fmt.Println("#######################################################")
+ fmt.Println("# STARTING PROXEUS IN TEST MODE - NOT FOR PRODUCTION #")
+ fmt.Println("#######################################################")
+ }
+
+ www.SetSystem(system)
+
embedded = &www.Embedded{Asset: assets.Asset}
sys.ReadAllFile = func(path string) ([]byte, error) {
return embedded.Asset(path)
}
- www.SetSystem(system)
go func() { //parse i18n from the UI assets to provide them under the translation section
i18nUIParser := i18n.NewUIParser()
@@ -122,14 +78,22 @@ func main() {
}
}()
- secure := www.NewSecurity()
+ e := www.Setup(ServerVersion)
- // Routes
- e.Pre(middleware.Secure())
+ // Static route
+ e.GET("/static/*", StaticHandler)
- api.ServerVersion = ServerVersion
+ // Initial config middleware
+ configured, err := system.Configured()
+ if err != nil && !os.IsNotExist(err) {
+ panic(err)
+ }
+ if !configured {
+ e.Use(www.NewInitialHandler(configured).Handler)
+ }
- handlers.MainHostedAPI(e, secure, system)
+ // Main routes
+ handlers.MainHostedAPI(e, www.NewSecurity(), ServerVersion)
www.StartServer(e, cfg.Config.ServiceAddress, false)
system.Shutdown()
diff --git a/main/www/apikey.go b/main/www/apikey.go
new file mode 100644
index 000000000..ecf95d9b8
--- /dev/null
+++ b/main/www/apikey.go
@@ -0,0 +1,56 @@
+package www
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo"
+
+ "git.proxeus.com/core/central/sys/session"
+)
+
+// SessionAuthToken create a request session if a valid API Key is found
+func SessionTokenAuth(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(e echo.Context) error {
+ c := e.(*Context)
+
+ if sess := c.Session(false); sess != nil {
+ return next(c)
+ }
+
+ // We first check if we can authenticate with an API key
+ sess, err := sessionFromSessionToken(c)
+ if err != nil {
+ // We had an session token but it not valid
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ var removeCookie bool
+ if sess != nil {
+ c.Set("sys.session", sess)
+ removeCookie = true
+ }
+
+ if err = next(c); err != nil {
+ c.Error(err)
+ }
+
+ if removeCookie {
+ c.Response().Header().Del("Set-Cookie")
+ }
+
+ return nil
+ }
+}
+
+func sessionFromSessionToken(c *Context) (*session.Session, error) {
+ token := c.SessionToken()
+ if token == "" {
+ return nil, nil
+ }
+
+ sess, err := c.System().SessionMgmnt.Get(token)
+ if err != nil {
+ return nil, err
+ }
+ return sess, nil
+}
diff --git a/main/www/apikey.go-e b/main/www/apikey.go-e
new file mode 100644
index 000000000..ecf95d9b8
--- /dev/null
+++ b/main/www/apikey.go-e
@@ -0,0 +1,56 @@
+package www
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo"
+
+ "git.proxeus.com/core/central/sys/session"
+)
+
+// SessionAuthToken create a request session if a valid API Key is found
+func SessionTokenAuth(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(e echo.Context) error {
+ c := e.(*Context)
+
+ if sess := c.Session(false); sess != nil {
+ return next(c)
+ }
+
+ // We first check if we can authenticate with an API key
+ sess, err := sessionFromSessionToken(c)
+ if err != nil {
+ // We had an session token but it not valid
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ var removeCookie bool
+ if sess != nil {
+ c.Set("sys.session", sess)
+ removeCookie = true
+ }
+
+ if err = next(c); err != nil {
+ c.Error(err)
+ }
+
+ if removeCookie {
+ c.Response().Header().Del("Set-Cookie")
+ }
+
+ return nil
+ }
+}
+
+func sessionFromSessionToken(c *Context) (*session.Session, error) {
+ token := c.SessionToken()
+ if token == "" {
+ return nil, nil
+ }
+
+ sess, err := c.System().SessionMgmnt.Get(token)
+ if err != nil {
+ return nil, err
+ }
+ return sess, nil
+}
diff --git a/main/www/context.go b/main/www/context.go
index b311577a7..544325051 100644
--- a/main/www/context.go
+++ b/main/www/context.go
@@ -1,7 +1,10 @@
package www
import (
+ "encoding/base64"
+ "errors"
"regexp"
+ "strings"
"github.com/labstack/echo"
@@ -45,27 +48,6 @@ func (me *Context) SessionWithUser(usr *model.User) *session.Session {
return sess
}
-//Auth checks if there is a session available otherwise it retrieves the api key if possible
-func (me *Context) Auth() (model.Authorization, error) {
- sess := me.Session(false)
- if sess == nil {
- u, err := useApiKeyAsUserAuth(me)
- if err != nil {
- return nil, err
- }
- return u, nil
- }
- return sess, nil
-}
-
-func (me *Context) ApiKey() (string, error) {
- _, apiKey := readApiKeyFromHeader(me.Request().Header.Get("Authorization"))
- if len(apiKey) > 0 {
- return apiKey, nil
- }
- return "", echo.ErrNotFound
-}
-
func (me *Context) EndSession() {
_ = delSession(me)
}
@@ -82,33 +64,63 @@ func (me *Context) I18n() *WebI18n {
return me.webI18n
}
-var apiKeyFromHeaderReg = regexp.MustCompile(`\s*(\w+)?\s*([^\s]*)`)
+var errInvalidRole = errors.New("the role of the user did not match")
-//Returns type and key as string.
-func readApiKeyFromHeader(headerValue string) (string, string) {
- subm := apiKeyFromHeaderReg.FindAllStringSubmatch(headerValue, 1)
+func (me *Context) EnsureUserRole(role model.Role) error {
+ sess := me.Session(false)
+ if sess == nil {
+ return errInvalidRole
+ }
+ user, err := me.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return errInvalidRole
+ }
+ if !user.IsGrantedFor(role) {
+ return errInvalidRole
+ }
+ return nil
+}
+
+// Extract the session token from the header
+func (me *Context) SessionToken() string {
+ return extractSessionToken(me.Request().Header.Get("Authorization"))
+}
+
+var sessionTokenFromHeaderReg = regexp.MustCompile(`^Bearer\s([^\s]+)$`)
+
+func extractSessionToken(headerValue string) string {
+ subm := sessionTokenFromHeaderReg.FindStringSubmatch(headerValue)
l := len(subm)
- if l == 1 {
- l = len(subm[0])
- if l == 3 {
- if len(subm[0][2]) == 0 {
- return "", subm[0][1]
- } else {
- return subm[0][1], subm[0][2]
- }
- }
+ if l != 2 {
+ return ""
}
- return "", ""
+ return subm[1]
}
-func useApiKeyAsUserAuth(c *Context) (model.Authorization, error) {
- apiKey, err := c.ApiKey()
- if err != nil {
- return nil, err
+// Extract the basic authentication from the header
+func (me *Context) BasicAuth() (string, string) {
+ return extractBasicAuth(me.Request().Header.Get("Authorization"))
+}
+
+var basicAuthFromHeaderReg = regexp.MustCompile(`^Basic\s([^\s]+)$`)
+
+func extractBasicAuth(headerValue string) (string, string) {
+ subm := basicAuthFromHeaderReg.FindStringSubmatch(headerValue)
+ l := len(subm)
+ if l != 2 {
+ return "", ""
}
- u, err := c.System().DB.User.APIKey(apiKey)
+
+ b, err := base64.StdEncoding.DecodeString(subm[1])
if err != nil {
- return nil, err
+ return "", ""
}
- return u, nil
+
+ fields := strings.Split(string(b), ":")
+
+ if len(fields) != 2 {
+ return "", ""
+ }
+
+ return strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1])
}
diff --git a/main/www/context.go-e b/main/www/context.go-e
index b311577a7..544325051 100644
--- a/main/www/context.go-e
+++ b/main/www/context.go-e
@@ -1,7 +1,10 @@
package www
import (
+ "encoding/base64"
+ "errors"
"regexp"
+ "strings"
"github.com/labstack/echo"
@@ -45,27 +48,6 @@ func (me *Context) SessionWithUser(usr *model.User) *session.Session {
return sess
}
-//Auth checks if there is a session available otherwise it retrieves the api key if possible
-func (me *Context) Auth() (model.Authorization, error) {
- sess := me.Session(false)
- if sess == nil {
- u, err := useApiKeyAsUserAuth(me)
- if err != nil {
- return nil, err
- }
- return u, nil
- }
- return sess, nil
-}
-
-func (me *Context) ApiKey() (string, error) {
- _, apiKey := readApiKeyFromHeader(me.Request().Header.Get("Authorization"))
- if len(apiKey) > 0 {
- return apiKey, nil
- }
- return "", echo.ErrNotFound
-}
-
func (me *Context) EndSession() {
_ = delSession(me)
}
@@ -82,33 +64,63 @@ func (me *Context) I18n() *WebI18n {
return me.webI18n
}
-var apiKeyFromHeaderReg = regexp.MustCompile(`\s*(\w+)?\s*([^\s]*)`)
+var errInvalidRole = errors.New("the role of the user did not match")
-//Returns type and key as string.
-func readApiKeyFromHeader(headerValue string) (string, string) {
- subm := apiKeyFromHeaderReg.FindAllStringSubmatch(headerValue, 1)
+func (me *Context) EnsureUserRole(role model.Role) error {
+ sess := me.Session(false)
+ if sess == nil {
+ return errInvalidRole
+ }
+ user, err := me.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return errInvalidRole
+ }
+ if !user.IsGrantedFor(role) {
+ return errInvalidRole
+ }
+ return nil
+}
+
+// Extract the session token from the header
+func (me *Context) SessionToken() string {
+ return extractSessionToken(me.Request().Header.Get("Authorization"))
+}
+
+var sessionTokenFromHeaderReg = regexp.MustCompile(`^Bearer\s([^\s]+)$`)
+
+func extractSessionToken(headerValue string) string {
+ subm := sessionTokenFromHeaderReg.FindStringSubmatch(headerValue)
l := len(subm)
- if l == 1 {
- l = len(subm[0])
- if l == 3 {
- if len(subm[0][2]) == 0 {
- return "", subm[0][1]
- } else {
- return subm[0][1], subm[0][2]
- }
- }
+ if l != 2 {
+ return ""
}
- return "", ""
+ return subm[1]
}
-func useApiKeyAsUserAuth(c *Context) (model.Authorization, error) {
- apiKey, err := c.ApiKey()
- if err != nil {
- return nil, err
+// Extract the basic authentication from the header
+func (me *Context) BasicAuth() (string, string) {
+ return extractBasicAuth(me.Request().Header.Get("Authorization"))
+}
+
+var basicAuthFromHeaderReg = regexp.MustCompile(`^Basic\s([^\s]+)$`)
+
+func extractBasicAuth(headerValue string) (string, string) {
+ subm := basicAuthFromHeaderReg.FindStringSubmatch(headerValue)
+ l := len(subm)
+ if l != 2 {
+ return "", ""
}
- u, err := c.System().DB.User.APIKey(apiKey)
+
+ b, err := base64.StdEncoding.DecodeString(subm[1])
if err != nil {
- return nil, err
+ return "", ""
}
- return u, nil
+
+ fields := strings.Split(string(b), ":")
+
+ if len(fields) != 2 {
+ return "", ""
+ }
+
+ return strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1])
}
diff --git a/main/www/context_test.go b/main/www/context_test.go
new file mode 100644
index 000000000..a4d8d84cf
--- /dev/null
+++ b/main/www/context_test.go
@@ -0,0 +1,135 @@
+package www
+
+import (
+ "encoding/base64"
+ "testing"
+)
+
+func TestExtractApiKey(t *testing.T) {
+
+ tests := []struct {
+ title string
+ value string
+ expected string
+ }{
+ {
+ "No header",
+ "",
+ "",
+ },
+ {
+ "Authorization header but wrong type",
+ "Basic 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing",
+ "Bearer 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing2",
+ " Bearer 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing3",
+ "Bearer 1234 ",
+ "",
+ },
+ {
+ "Good",
+ "Bearer 1234",
+ "1234",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.title, func(t *testing.T) {
+ result := extractSessionToken(test.value)
+ if result != test.expected {
+ t.Errorf("Expected %s and got %s", test.expected, result)
+ }
+ })
+ }
+
+}
+
+func TestExtractBasicAuth(t *testing.T) {
+ tests := []struct {
+ title string
+ value string
+ user string
+ password string
+ }{
+ {
+ "No header",
+ "",
+ "",
+ "",
+ },
+ {
+ "Authorization header but wrong type",
+ "Bearer 1234",
+ "",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing2",
+ " Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong spacing3",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")) + " ",
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:")),
+ "foo",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte(":bar")),
+ "",
+ "bar",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte(":")),
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("")),
+ "",
+ "",
+ },
+ {
+ "Good",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "foo",
+ "bar",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.title, func(t *testing.T) {
+ u, p := extractBasicAuth(test.value)
+ if u != test.user || p != test.password {
+ t.Errorf("Expected %s:%s and got %s:%s", test.user, test.password, u, p)
+ }
+ })
+ }
+}
diff --git a/main/www/context_test.go-e b/main/www/context_test.go-e
new file mode 100644
index 000000000..a4d8d84cf
--- /dev/null
+++ b/main/www/context_test.go-e
@@ -0,0 +1,135 @@
+package www
+
+import (
+ "encoding/base64"
+ "testing"
+)
+
+func TestExtractApiKey(t *testing.T) {
+
+ tests := []struct {
+ title string
+ value string
+ expected string
+ }{
+ {
+ "No header",
+ "",
+ "",
+ },
+ {
+ "Authorization header but wrong type",
+ "Basic 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing",
+ "Bearer 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing2",
+ " Bearer 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing3",
+ "Bearer 1234 ",
+ "",
+ },
+ {
+ "Good",
+ "Bearer 1234",
+ "1234",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.title, func(t *testing.T) {
+ result := extractSessionToken(test.value)
+ if result != test.expected {
+ t.Errorf("Expected %s and got %s", test.expected, result)
+ }
+ })
+ }
+
+}
+
+func TestExtractBasicAuth(t *testing.T) {
+ tests := []struct {
+ title string
+ value string
+ user string
+ password string
+ }{
+ {
+ "No header",
+ "",
+ "",
+ "",
+ },
+ {
+ "Authorization header but wrong type",
+ "Bearer 1234",
+ "",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing2",
+ " Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong spacing3",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")) + " ",
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:")),
+ "foo",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte(":bar")),
+ "",
+ "bar",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte(":")),
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("")),
+ "",
+ "",
+ },
+ {
+ "Good",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "foo",
+ "bar",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.title, func(t *testing.T) {
+ u, p := extractBasicAuth(test.value)
+ if u != test.user || p != test.password {
+ t.Errorf("Expected %s:%s and got %s:%s", test.user, test.password, u, p)
+ }
+ })
+ }
+}
diff --git a/main/www/default_server.go b/main/www/default_server.go
deleted file mode 100644
index cd99eec1d..000000000
--- a/main/www/default_server.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package www
-
-import (
- "context"
- "fmt"
- "io"
- "log"
- "os"
- "os/signal"
- "path"
- "syscall"
-
- "golang.org/x/crypto/acme/autocert"
-
- "github.com/labstack/echo"
- "github.com/labstack/echo/middleware"
- "github.com/natefinch/lumberjack"
-)
-
-func Setup(logFileLocation string) *echo.Echo {
- e := echo.New()
-
- // logging setup
- {
- e.Debug = true
- var lw io.Writer
- lw = &lumberjack.Logger{
- Filename: logFileLocation,
- MaxSize: 100, // MB
- MaxAge: 120, // days
- }
- // test it
- _, err := lw.Write([]byte("log init\n"))
- if err != nil {
- log.Printf("File logging disabled due to: <%s>\n", err)
- // fallback to std
- lw = os.Stdout
- } else {
- log.Printf("Logging to: %s\n", logFileLocation)
- }
- e.Logger.SetOutput(lw)
- log.SetOutput(lw)
- e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{Output: lw}))
- }
-
- // very important
- e.Use(middleware.Recover())
-
- e.HTTPErrorHandler = DefaultHTTPErrorHandler
- e.HideBanner = true
-
- return e
-}
-
-func StartServer(e *echo.Echo, addr string, autoTLS bool) {
- if autoTLS {
- dirCache := path.Join(os.TempDir(), ".cache")
- e.AutoTLSManager.Cache = autocert.DirCache(dirCache)
- }
- quit := make(chan os.Signal)
-
- // Start server
- go func() {
- if autoTLS {
- fmt.Println("starting https at", addr)
- if err := e.StartAutoTLS(addr); err != nil {
- log.Println(err)
- }
- } else {
- fmt.Println("starting plain http at", addr)
- if err := e.Start(addr); err != nil {
- log.Println(err)
- }
- }
- }()
-
- signal.Notify(quit, os.Interrupt)
- signal.Notify(quit, syscall.SIGTERM)
- <-quit
- if err := e.Shutdown(context.Background()); err != nil {
- log.Fatal(err)
- }
-}
diff --git a/main/www/default_server.go-e b/main/www/default_server.go-e
deleted file mode 100644
index cd99eec1d..000000000
--- a/main/www/default_server.go-e
+++ /dev/null
@@ -1,83 +0,0 @@
-package www
-
-import (
- "context"
- "fmt"
- "io"
- "log"
- "os"
- "os/signal"
- "path"
- "syscall"
-
- "golang.org/x/crypto/acme/autocert"
-
- "github.com/labstack/echo"
- "github.com/labstack/echo/middleware"
- "github.com/natefinch/lumberjack"
-)
-
-func Setup(logFileLocation string) *echo.Echo {
- e := echo.New()
-
- // logging setup
- {
- e.Debug = true
- var lw io.Writer
- lw = &lumberjack.Logger{
- Filename: logFileLocation,
- MaxSize: 100, // MB
- MaxAge: 120, // days
- }
- // test it
- _, err := lw.Write([]byte("log init\n"))
- if err != nil {
- log.Printf("File logging disabled due to: <%s>\n", err)
- // fallback to std
- lw = os.Stdout
- } else {
- log.Printf("Logging to: %s\n", logFileLocation)
- }
- e.Logger.SetOutput(lw)
- log.SetOutput(lw)
- e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{Output: lw}))
- }
-
- // very important
- e.Use(middleware.Recover())
-
- e.HTTPErrorHandler = DefaultHTTPErrorHandler
- e.HideBanner = true
-
- return e
-}
-
-func StartServer(e *echo.Echo, addr string, autoTLS bool) {
- if autoTLS {
- dirCache := path.Join(os.TempDir(), ".cache")
- e.AutoTLSManager.Cache = autocert.DirCache(dirCache)
- }
- quit := make(chan os.Signal)
-
- // Start server
- go func() {
- if autoTLS {
- fmt.Println("starting https at", addr)
- if err := e.StartAutoTLS(addr); err != nil {
- log.Println(err)
- }
- } else {
- fmt.Println("starting plain http at", addr)
- if err := e.Start(addr); err != nil {
- log.Println(err)
- }
- }
- }()
-
- signal.Notify(quit, os.Interrupt)
- signal.Notify(quit, syscall.SIGTERM)
- <-quit
- if err := e.Shutdown(context.Background()); err != nil {
- log.Fatal(err)
- }
-}
diff --git a/main/www/error.go b/main/www/error.go
index b459891ad..08bb1654e 100644
--- a/main/www/error.go
+++ b/main/www/error.go
@@ -25,22 +25,28 @@ func DefaultHTTPErrorHandler(err error, c echo.Context) {
if _, ok := msg.(string); ok {
msg = echo.Map{"message": msg}
}
- if !c.Response().Committed {
- if c.Request().Header.Get("X-Requested-With") == "XMLHttpRequest" {
- if err = c.JSON(code, msg); err != nil {
- goto ERROR
- }
- } else {
- bts, err = sys.ReadAllFile("frontend.html")
- if err == nil {
- if err = c.HTMLBlob(code, bts); err != nil {
- goto ERROR
- }
- return
- }
+ if c.Response().Committed {
+ log.Println(err)
+ return
+ }
+
+ if c.Request().Header.Get("X-Requested-With") == "XMLHttpRequest" {
+ err := c.JSON(code, msg)
+ if err != nil {
+ log.Println(err)
}
+ return
+ }
+
+ bts, err = sys.ReadAllFile("frontend.html")
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ err = c.HTMLBlob(code, bts)
+ if err != nil {
+ log.Println(err)
}
-ERROR:
- log.Println(err)
}
diff --git a/main/www/error.go-e b/main/www/error.go-e
index b459891ad..08bb1654e 100644
--- a/main/www/error.go-e
+++ b/main/www/error.go-e
@@ -25,22 +25,28 @@ func DefaultHTTPErrorHandler(err error, c echo.Context) {
if _, ok := msg.(string); ok {
msg = echo.Map{"message": msg}
}
- if !c.Response().Committed {
- if c.Request().Header.Get("X-Requested-With") == "XMLHttpRequest" {
- if err = c.JSON(code, msg); err != nil {
- goto ERROR
- }
- } else {
- bts, err = sys.ReadAllFile("frontend.html")
- if err == nil {
- if err = c.HTMLBlob(code, bts); err != nil {
- goto ERROR
- }
- return
- }
+ if c.Response().Committed {
+ log.Println(err)
+ return
+ }
+
+ if c.Request().Header.Get("X-Requested-With") == "XMLHttpRequest" {
+ err := c.JSON(code, msg)
+ if err != nil {
+ log.Println(err)
}
+ return
+ }
+
+ bts, err = sys.ReadAllFile("frontend.html")
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ err = c.HTMLBlob(code, bts)
+ if err != nil {
+ log.Println(err)
}
-ERROR:
- log.Println(err)
}
diff --git a/main/www/initial.go b/main/www/initial.go
index 5e102dbfd..8f7015dc2 100644
--- a/main/www/initial.go
+++ b/main/www/initial.go
@@ -22,48 +22,50 @@ func NewInitialHandler(configured bool) *InitialHandler {
func (me *InitialHandler) Handler(next echo.HandlerFunc) echo.HandlerFunc {
return func(e echo.Context) error {
c := e.(*Context)
- if !me.configured {
- sess := c.Session(false)
- if sess == nil || sess.AccessRights() != model.ROOT { //ensure we have a tmp root session to power up
- sess = c.SessionWithUser(&model.User{ID: "XYZ", Role: model.ROOT})
- }
+ if me.configured {
+ return next(c)
+ }
- if me.cleanOnNextCall && c.Request().RequestURI != "/api/import/results" && c.Request().RequestURI != "/api/init" {
- me.configured, _ = c.System().Configured()
- me.cleanOnNextCall = false
- er2 := c.System().SessionMgmnt.Clean()
- if er2 != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
- return next(c)
+ sess := c.Session(false)
+ if sess == nil || sess.AccessRights() != model.ROOT { //ensure we have a tmp root session to power up
+ sess = c.SessionWithUser(&model.User{ID: "XYZ", Role: model.ROOT})
+ }
+
+ if me.cleanOnNextCall && c.Request().RequestURI != "/api/import/results" && c.Request().RequestURI != "/api/init" {
+ me.configured, _ = c.System().Configured()
+ me.cleanOnNextCall = false
+ er2 := c.System().SessionMgmnt.Clean()
+ if er2 != nil {
+ return c.NoContent(http.StatusInternalServerError)
}
- if strings.ToLower(c.Request().Method) == "get" {
- if !strings.HasPrefix(c.Request().RequestURI, "/api/") &&
- !strings.HasPrefix(c.Request().RequestURI, "/static/") &&
- !strings.HasPrefix(c.Request().RequestURI, "/favicon.ico") {
- bts, err := sys.ReadAllFile("initial.html")
- if err != nil {
- return c.NoContent(http.StatusNotFound)
- }
- return c.HTMLBlob(http.StatusOK, bts)
- }
- return next(c)
- } else {
- if strings.HasPrefix(c.Request().RequestURI, "/api/init") || strings.HasPrefix(c.Request().RequestURI, "/api/import") {
- er := next(c)
- me.configured, _ = c.System().Configured()
- if me.configured {
- me.configured = false
- //to let /api/import/results through as all sessions will be deleted afterwards, this makes it possible
- //to view the results before they are gone
- me.cleanOnNextCall = true
- }
- return er
+ return next(c)
+ }
+
+ if strings.ToLower(c.Request().Method) == "get" {
+ if !strings.HasPrefix(c.Request().RequestURI, "/api/") &&
+ !strings.HasPrefix(c.Request().RequestURI, "/static/") &&
+ !strings.HasPrefix(c.Request().RequestURI, "/favicon.ico") {
+ bts, err := sys.ReadAllFile("initial.html")
+ if err != nil {
+ return c.NoContent(http.StatusNotFound)
}
- return c.NoContent(http.StatusBadRequest)
+ return c.HTMLBlob(http.StatusOK, bts)
}
+ return next(c)
+ }
+ if strings.HasPrefix(c.Request().RequestURI, "/api/init") || strings.HasPrefix(c.Request().RequestURI, "/api/import") {
+ er := next(c)
+ me.configured, _ = c.System().Configured()
+ if me.configured {
+ me.configured = false
+ //to let /api/import/results through as all sessions will be deleted afterwards, this makes it possible
+ //to view the results before they are gone
+ me.cleanOnNextCall = true
+ }
+ return er
}
- return next(c)
+
+ return c.NoContent(http.StatusBadRequest)
}
}
diff --git a/main/www/initial.go-e b/main/www/initial.go-e
index 5e102dbfd..8f7015dc2 100644
--- a/main/www/initial.go-e
+++ b/main/www/initial.go-e
@@ -22,48 +22,50 @@ func NewInitialHandler(configured bool) *InitialHandler {
func (me *InitialHandler) Handler(next echo.HandlerFunc) echo.HandlerFunc {
return func(e echo.Context) error {
c := e.(*Context)
- if !me.configured {
- sess := c.Session(false)
- if sess == nil || sess.AccessRights() != model.ROOT { //ensure we have a tmp root session to power up
- sess = c.SessionWithUser(&model.User{ID: "XYZ", Role: model.ROOT})
- }
+ if me.configured {
+ return next(c)
+ }
- if me.cleanOnNextCall && c.Request().RequestURI != "/api/import/results" && c.Request().RequestURI != "/api/init" {
- me.configured, _ = c.System().Configured()
- me.cleanOnNextCall = false
- er2 := c.System().SessionMgmnt.Clean()
- if er2 != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
- return next(c)
+ sess := c.Session(false)
+ if sess == nil || sess.AccessRights() != model.ROOT { //ensure we have a tmp root session to power up
+ sess = c.SessionWithUser(&model.User{ID: "XYZ", Role: model.ROOT})
+ }
+
+ if me.cleanOnNextCall && c.Request().RequestURI != "/api/import/results" && c.Request().RequestURI != "/api/init" {
+ me.configured, _ = c.System().Configured()
+ me.cleanOnNextCall = false
+ er2 := c.System().SessionMgmnt.Clean()
+ if er2 != nil {
+ return c.NoContent(http.StatusInternalServerError)
}
- if strings.ToLower(c.Request().Method) == "get" {
- if !strings.HasPrefix(c.Request().RequestURI, "/api/") &&
- !strings.HasPrefix(c.Request().RequestURI, "/static/") &&
- !strings.HasPrefix(c.Request().RequestURI, "/favicon.ico") {
- bts, err := sys.ReadAllFile("initial.html")
- if err != nil {
- return c.NoContent(http.StatusNotFound)
- }
- return c.HTMLBlob(http.StatusOK, bts)
- }
- return next(c)
- } else {
- if strings.HasPrefix(c.Request().RequestURI, "/api/init") || strings.HasPrefix(c.Request().RequestURI, "/api/import") {
- er := next(c)
- me.configured, _ = c.System().Configured()
- if me.configured {
- me.configured = false
- //to let /api/import/results through as all sessions will be deleted afterwards, this makes it possible
- //to view the results before they are gone
- me.cleanOnNextCall = true
- }
- return er
+ return next(c)
+ }
+
+ if strings.ToLower(c.Request().Method) == "get" {
+ if !strings.HasPrefix(c.Request().RequestURI, "/api/") &&
+ !strings.HasPrefix(c.Request().RequestURI, "/static/") &&
+ !strings.HasPrefix(c.Request().RequestURI, "/favicon.ico") {
+ bts, err := sys.ReadAllFile("initial.html")
+ if err != nil {
+ return c.NoContent(http.StatusNotFound)
}
- return c.NoContent(http.StatusBadRequest)
+ return c.HTMLBlob(http.StatusOK, bts)
}
+ return next(c)
+ }
+ if strings.HasPrefix(c.Request().RequestURI, "/api/init") || strings.HasPrefix(c.Request().RequestURI, "/api/import") {
+ er := next(c)
+ me.configured, _ = c.System().Configured()
+ if me.configured {
+ me.configured = false
+ //to let /api/import/results through as all sessions will be deleted afterwards, this makes it possible
+ //to view the results before they are gone
+ me.cleanOnNextCall = true
+ }
+ return er
}
- return next(c)
+
+ return c.NoContent(http.StatusBadRequest)
}
}
diff --git a/main/www/server.go b/main/www/server.go
index 837eb88cd..473f72601 100644
--- a/main/www/server.go
+++ b/main/www/server.go
@@ -1,80 +1,106 @@
package www
import (
- "bytes"
- "io"
- "io/ioutil"
+ "context"
+ "fmt"
+ "log"
"os"
+ "os/signal"
+ "path"
+ "syscall"
- "path/filepath"
-)
+ "golang.org/x/crypto/acme/autocert"
-type MyServer struct {
- quit chan os.Signal
-}
+ "github.com/labstack/echo"
+ "github.com/labstack/echo/middleware"
+)
-func (ms *MyServer) Close() {
- if ms.quit != nil {
- ms.quit <- os.Interrupt
- }
-}
+func Setup(serverVersion string) *echo.Echo {
+ e := echo.New()
+ e.HTTPErrorHandler = DefaultHTTPErrorHandler
-type MyHTMLTemplateLoader struct {
- BaseDir string
- MoreDirs *[]string
-}
+ // Pre routing middleware
+ e.Pre(xVersionHeader(serverVersion))
+ c := middleware.DefaultSecureConfig
+ c.XFrameOptions = ""
+ e.Pre(middleware.SecureWithConfig(c))
+ e.Pre(middleware.Secure())
-// Abs calculates the path to a given template. Whenever a path must be resolved
-// due to an import from another template, the base equals the parent template's path.
-func (htl *MyHTMLTemplateLoader) Abs(base, name string) (absPath string) {
- if filepath.IsAbs(name) {
- return name
- }
+ // Post routing middleware
+ e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ return h(&Context{Context: c})
+ }
+ })
+ //Simple Request Logging
+ e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
+ Format: "[echo] ${time_rfc3339} client=${remote_ip}, method=${method}, uri=${uri}, status=${status}\n",
+ }))
- // Our own base dir has always priority; if there's none
- // we use the path provided in base.
- var err error
- if htl.BaseDir == "" {
- if base == "" {
- base, err = os.Getwd()
+ //Request Logging with User Info and Body on Error
+ e.Use(middleware.BodyDump(func(e echo.Context, reqBody, resBody []byte) {
+ c := e.(*Context)
+ s := c.Session(false)
+ if s == nil {
+ return
+ }
+ if s.ID() != "" {
+ id := s.UserID()
+ user, err := c.System().DB.User.Get(s, id)
if err != nil {
- panic(err)
+ return
+ }
+ userName := user.Name
+ userAddr := user.EthereumAddr
+ log.Println("[echo] Method: "+e.Request().Method, "Status:", e.Response().Status, "User: "+userAddr, "("+userName+")", "URI: "+e.Request().RequestURI)
+ if len(reqBody) > 0 && c.Response().Status != 200 && c.Response().Status != 404 {
+ fmt.Printf("[echo][errorrequest] %s\n", reqBody)
}
- absPath = filepath.Join(base, name)
- htl.checkPath(&name, &absPath)
- return
}
- absPath = filepath.Join(filepath.Dir(base), name)
- htl.checkPath(&name, &absPath)
- return
- }
- absPath = filepath.Join(htl.BaseDir, name)
- htl.checkPath(&name, &absPath)
- return
+
+ }))
+
+ e.Use(SessionMiddleware())
+ e.Use(SessionTokenAuth)
+
+ return e
}
-func (htl *MyHTMLTemplateLoader) checkPath(relPath, absPath *string) {
- if htl.MoreDirs != nil && len(*htl.MoreDirs) > 0 {
- var err error
- if _, err = os.Stat(*absPath); err == nil {
- return
- }
- newPath := ""
- for _, dirPath := range *htl.MoreDirs {
- newPath = filepath.Join(dirPath, *relPath)
- if _, err = os.Stat(newPath); err == nil {
- *absPath = newPath
- break
+func StartServer(e *echo.Echo, addr string, autoTLS bool) {
+ if autoTLS {
+ dirCache := path.Join(os.TempDir(), ".cache")
+ e.AutoTLSManager.Cache = autocert.DirCache(dirCache)
+ }
+ quit := make(chan os.Signal)
+
+ // Start server
+ go func() {
+ if autoTLS {
+ fmt.Println("starting https at", addr)
+ if err := e.StartAutoTLS(addr); err != nil {
+ log.Println(err)
+ }
+ } else {
+ fmt.Println("starting plain http at", addr)
+ if err := e.Start(addr); err != nil {
+ log.Println(err)
}
}
+ }()
+
+ signal.Notify(quit, os.Interrupt)
+ signal.Notify(quit, syscall.SIGTERM)
+ <-quit
+ if err := e.Shutdown(context.Background()); err != nil {
+ log.Fatal(err)
}
}
-// Get returns an io.Reader where the template's content can be read from.
-func (htl *MyHTMLTemplateLoader) Get(path string) (io.Reader, error) {
- buf, err := ioutil.ReadFile(path)
- if err != nil {
- return nil, err
+func xVersionHeader(version string) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ c.Response().Header().Set("X-Version", version)
+ return next(c)
+ }
}
- return bytes.NewReader(buf), nil
}
diff --git a/main/www/server.go-e b/main/www/server.go-e
index 837eb88cd..473f72601 100644
--- a/main/www/server.go-e
+++ b/main/www/server.go-e
@@ -1,80 +1,106 @@
package www
import (
- "bytes"
- "io"
- "io/ioutil"
+ "context"
+ "fmt"
+ "log"
"os"
+ "os/signal"
+ "path"
+ "syscall"
- "path/filepath"
-)
+ "golang.org/x/crypto/acme/autocert"
-type MyServer struct {
- quit chan os.Signal
-}
+ "github.com/labstack/echo"
+ "github.com/labstack/echo/middleware"
+)
-func (ms *MyServer) Close() {
- if ms.quit != nil {
- ms.quit <- os.Interrupt
- }
-}
+func Setup(serverVersion string) *echo.Echo {
+ e := echo.New()
+ e.HTTPErrorHandler = DefaultHTTPErrorHandler
-type MyHTMLTemplateLoader struct {
- BaseDir string
- MoreDirs *[]string
-}
+ // Pre routing middleware
+ e.Pre(xVersionHeader(serverVersion))
+ c := middleware.DefaultSecureConfig
+ c.XFrameOptions = ""
+ e.Pre(middleware.SecureWithConfig(c))
+ e.Pre(middleware.Secure())
-// Abs calculates the path to a given template. Whenever a path must be resolved
-// due to an import from another template, the base equals the parent template's path.
-func (htl *MyHTMLTemplateLoader) Abs(base, name string) (absPath string) {
- if filepath.IsAbs(name) {
- return name
- }
+ // Post routing middleware
+ e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ return h(&Context{Context: c})
+ }
+ })
+ //Simple Request Logging
+ e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
+ Format: "[echo] ${time_rfc3339} client=${remote_ip}, method=${method}, uri=${uri}, status=${status}\n",
+ }))
- // Our own base dir has always priority; if there's none
- // we use the path provided in base.
- var err error
- if htl.BaseDir == "" {
- if base == "" {
- base, err = os.Getwd()
+ //Request Logging with User Info and Body on Error
+ e.Use(middleware.BodyDump(func(e echo.Context, reqBody, resBody []byte) {
+ c := e.(*Context)
+ s := c.Session(false)
+ if s == nil {
+ return
+ }
+ if s.ID() != "" {
+ id := s.UserID()
+ user, err := c.System().DB.User.Get(s, id)
if err != nil {
- panic(err)
+ return
+ }
+ userName := user.Name
+ userAddr := user.EthereumAddr
+ log.Println("[echo] Method: "+e.Request().Method, "Status:", e.Response().Status, "User: "+userAddr, "("+userName+")", "URI: "+e.Request().RequestURI)
+ if len(reqBody) > 0 && c.Response().Status != 200 && c.Response().Status != 404 {
+ fmt.Printf("[echo][errorrequest] %s\n", reqBody)
}
- absPath = filepath.Join(base, name)
- htl.checkPath(&name, &absPath)
- return
}
- absPath = filepath.Join(filepath.Dir(base), name)
- htl.checkPath(&name, &absPath)
- return
- }
- absPath = filepath.Join(htl.BaseDir, name)
- htl.checkPath(&name, &absPath)
- return
+
+ }))
+
+ e.Use(SessionMiddleware())
+ e.Use(SessionTokenAuth)
+
+ return e
}
-func (htl *MyHTMLTemplateLoader) checkPath(relPath, absPath *string) {
- if htl.MoreDirs != nil && len(*htl.MoreDirs) > 0 {
- var err error
- if _, err = os.Stat(*absPath); err == nil {
- return
- }
- newPath := ""
- for _, dirPath := range *htl.MoreDirs {
- newPath = filepath.Join(dirPath, *relPath)
- if _, err = os.Stat(newPath); err == nil {
- *absPath = newPath
- break
+func StartServer(e *echo.Echo, addr string, autoTLS bool) {
+ if autoTLS {
+ dirCache := path.Join(os.TempDir(), ".cache")
+ e.AutoTLSManager.Cache = autocert.DirCache(dirCache)
+ }
+ quit := make(chan os.Signal)
+
+ // Start server
+ go func() {
+ if autoTLS {
+ fmt.Println("starting https at", addr)
+ if err := e.StartAutoTLS(addr); err != nil {
+ log.Println(err)
+ }
+ } else {
+ fmt.Println("starting plain http at", addr)
+ if err := e.Start(addr); err != nil {
+ log.Println(err)
}
}
+ }()
+
+ signal.Notify(quit, os.Interrupt)
+ signal.Notify(quit, syscall.SIGTERM)
+ <-quit
+ if err := e.Shutdown(context.Background()); err != nil {
+ log.Fatal(err)
}
}
-// Get returns an io.Reader where the template's content can be read from.
-func (htl *MyHTMLTemplateLoader) Get(path string) (io.Reader, error) {
- buf, err := ioutil.ReadFile(path)
- if err != nil {
- return nil, err
+func xVersionHeader(version string) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ c.Response().Header().Set("X-Version", version)
+ return next(c)
+ }
}
- return bytes.NewReader(buf), nil
}
diff --git a/main/www/session.go b/main/www/session.go
index 031af2af2..7e7c14490 100644
--- a/main/www/session.go
+++ b/main/www/session.go
@@ -14,11 +14,11 @@ import (
sysSess "git.proxeus.com/core/central/sys/session"
)
-func SetupSession(e *echo.Echo) {
+func SessionMiddleware() echo.MiddlewareFunc {
gob.Register(map[string]interface{}{})
gob.Register(map[string]map[string]string{})
sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
- e.Use(session.Middleware(sessionStore))
+ return session.Middleware(sessionStore)
}
var anonymousUser = &model.User{Role: model.PUBLIC}
@@ -30,17 +30,16 @@ func init() {
}
func getSessionWithUser(c *Context, create bool, usr *model.User) (currentSession *sysSess.Session, err error) {
- var sess *sessions.Session
- var ok bool
if !create || usr == nil {
if csess := c.Get("sys.session"); csess != nil {
+ var ok bool
if currentSession, ok = csess.(*sysSess.Session); ok {
return
}
}
}
- sess, err = session.Get("s", c)
+ sess, err := session.Get("s", c)
if sess == nil || err != nil {
return
}
diff --git a/main/www/session.go-e b/main/www/session.go-e
index 031af2af2..7e7c14490 100644
--- a/main/www/session.go-e
+++ b/main/www/session.go-e
@@ -14,11 +14,11 @@ import (
sysSess "git.proxeus.com/core/central/sys/session"
)
-func SetupSession(e *echo.Echo) {
+func SessionMiddleware() echo.MiddlewareFunc {
gob.Register(map[string]interface{}{})
gob.Register(map[string]map[string]string{})
sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
- e.Use(session.Middleware(sessionStore))
+ return session.Middleware(sessionStore)
}
var anonymousUser = &model.User{Role: model.PUBLIC}
@@ -30,17 +30,16 @@ func init() {
}
func getSessionWithUser(c *Context, create bool, usr *model.User) (currentSession *sysSess.Session, err error) {
- var sess *sessions.Session
- var ok bool
if !create || usr == nil {
if csess := c.Get("sys.session"); csess != nil {
+ var ok bool
if currentSession, ok = csess.(*sysSess.Session); ok {
return
}
}
}
- sess, err = session.Get("s", c)
+ sess, err := session.Get("s", c)
if sess == nil || err != nil {
return
}
diff --git a/sys/db/storm/user_data.go b/sys/db/storm/user_data.go
index 31022d653..0ffdc75b8 100644
--- a/sys/db/storm/user_data.go
+++ b/sys/db/storm/user_data.go
@@ -47,8 +47,14 @@ func NewUserDataDB(dir string) (*UserDataDB, error) {
udb.baseFilePath = assetDir
example := &model.UserDataItem{}
- udb.db.Init(example)
- udb.db.ReIndex(example)
+ err = udb.db.Init(example)
+ if err != nil {
+ return nil, err
+ }
+ err = udb.db.ReIndex(example)
+ if err != nil {
+ return nil, err
+ }
var fVersion int
verr := udb.db.Get(usrdVersion, usrdVersion, &fVersion)
if verr == nil && fVersion != example.GetVersion() {
@@ -146,30 +152,21 @@ func (me *UserDataDB) GetAllFileInfosOf(ud *model.UserDataItem) []*file.IO {
return m.GetAllFileInfos(me.baseFilePath)
}
-func (me *UserDataDB) GetByWorkflow(auth model.Authorization, wf *model.WorkflowItem, finished bool) (*model.UserDataItem, error) {
+func (me *UserDataDB) GetByWorkflow(auth model.Authorization, wf *model.WorkflowItem, finished bool) (*model.UserDataItem, bool, error) {
var item model.UserDataItem
matchers := defaultMatcher(auth, "", nil, true)
matchers = append(matchers, q.And(q.Eq("WorkflowID", wf.ID), q.Eq("Finished", finished)))
+ alreadyStarted := false
err := me.db.Select(matchers...).OrderBy("Created").Reverse().First(&item)
- if err == storm.ErrNotFound && !finished {
- item.WorkflowID = wf.ID
- item.Name = wf.Name
- item.Detail = wf.Detail
- er := me.Put(auth, &item)
- if er != nil {
- return nil, er
- } else {
- return &item, nil
- }
- }
if err != nil {
- return nil, err
+ return nil, alreadyStarted, err
}
+ alreadyStarted = true
if !item.Permissions.IsReadGrantedFor(auth) {
- return nil, model.ErrAuthorityMissing
+ return nil, alreadyStarted, model.ErrAuthorityMissing
}
- me.db.Get(usrdHeavyData, item.ID, &item.Data)
- return &item, nil
+ err = me.db.Get(usrdHeavyData, item.ID, &item.Data)
+ return &item, alreadyStarted, err
}
func (me *UserDataDB) GetData(auth model.Authorization, id, dataPath string) (interface{}, error) {
diff --git a/sys/db/storm/user_data.go-e b/sys/db/storm/user_data.go-e
index 31022d653..0ffdc75b8 100644
--- a/sys/db/storm/user_data.go-e
+++ b/sys/db/storm/user_data.go-e
@@ -47,8 +47,14 @@ func NewUserDataDB(dir string) (*UserDataDB, error) {
udb.baseFilePath = assetDir
example := &model.UserDataItem{}
- udb.db.Init(example)
- udb.db.ReIndex(example)
+ err = udb.db.Init(example)
+ if err != nil {
+ return nil, err
+ }
+ err = udb.db.ReIndex(example)
+ if err != nil {
+ return nil, err
+ }
var fVersion int
verr := udb.db.Get(usrdVersion, usrdVersion, &fVersion)
if verr == nil && fVersion != example.GetVersion() {
@@ -146,30 +152,21 @@ func (me *UserDataDB) GetAllFileInfosOf(ud *model.UserDataItem) []*file.IO {
return m.GetAllFileInfos(me.baseFilePath)
}
-func (me *UserDataDB) GetByWorkflow(auth model.Authorization, wf *model.WorkflowItem, finished bool) (*model.UserDataItem, error) {
+func (me *UserDataDB) GetByWorkflow(auth model.Authorization, wf *model.WorkflowItem, finished bool) (*model.UserDataItem, bool, error) {
var item model.UserDataItem
matchers := defaultMatcher(auth, "", nil, true)
matchers = append(matchers, q.And(q.Eq("WorkflowID", wf.ID), q.Eq("Finished", finished)))
+ alreadyStarted := false
err := me.db.Select(matchers...).OrderBy("Created").Reverse().First(&item)
- if err == storm.ErrNotFound && !finished {
- item.WorkflowID = wf.ID
- item.Name = wf.Name
- item.Detail = wf.Detail
- er := me.Put(auth, &item)
- if er != nil {
- return nil, er
- } else {
- return &item, nil
- }
- }
if err != nil {
- return nil, err
+ return nil, alreadyStarted, err
}
+ alreadyStarted = true
if !item.Permissions.IsReadGrantedFor(auth) {
- return nil, model.ErrAuthorityMissing
+ return nil, alreadyStarted, model.ErrAuthorityMissing
}
- me.db.Get(usrdHeavyData, item.ID, &item.Data)
- return &item, nil
+ err = me.db.Get(usrdHeavyData, item.ID, &item.Data)
+ return &item, alreadyStarted, err
}
func (me *UserDataDB) GetData(auth model.Authorization, id, dataPath string) (interface{}, error) {
diff --git a/sys/db/storm/user_data_test.go b/sys/db/storm/user_data_test.go
index cf1d2b9e4..6fbcb02d5 100644
--- a/sys/db/storm/user_data_test.go
+++ b/sys/db/storm/user_data_test.go
@@ -40,7 +40,7 @@ func TestPutGetData(t *testing.T) {
t.Error("data is nil")
}
if innerMap, ok := newUsrData.Data["input"].(map[string]interface{}); ok {
- if someInt, ok := innerMap["someInt"].(uint16); ok {
+ if someInt, ok := innerMap["someInt"].(int64); ok {
if someInt != 1234 {
t.Error("someInt missing", someInt)
}
@@ -48,16 +48,16 @@ func TestPutGetData(t *testing.T) {
t.Error("someInt missing")
}
if list, ok := innerMap["list"].([]interface{}); ok {
- if i, ok := list[0].(uint16); ok && i != 1 {
+ if i, ok := list[0].(int64); ok && i != 1 {
t.Error("not 1")
}
- if i, ok := list[1].(uint16); ok && i != 2 {
+ if i, ok := list[1].(int64); ok && i != 2 {
t.Error("not 2")
}
- if i, ok := list[2].(uint16); ok && i != 3 {
+ if i, ok := list[2].(int64); ok && i != 3 {
t.Error("not 3")
}
- if i, ok := list[3].(uint16); ok && i != 4 {
+ if i, ok := list[3].(int64); ok && i != 4 {
t.Error("not 4")
}
if i, ok := list[4].(string); ok && i != "hello" {
diff --git a/sys/db/storm/user_data_test.go-e b/sys/db/storm/user_data_test.go-e
index cf1d2b9e4..6fbcb02d5 100644
--- a/sys/db/storm/user_data_test.go-e
+++ b/sys/db/storm/user_data_test.go-e
@@ -40,7 +40,7 @@ func TestPutGetData(t *testing.T) {
t.Error("data is nil")
}
if innerMap, ok := newUsrData.Data["input"].(map[string]interface{}); ok {
- if someInt, ok := innerMap["someInt"].(uint16); ok {
+ if someInt, ok := innerMap["someInt"].(int64); ok {
if someInt != 1234 {
t.Error("someInt missing", someInt)
}
@@ -48,16 +48,16 @@ func TestPutGetData(t *testing.T) {
t.Error("someInt missing")
}
if list, ok := innerMap["list"].([]interface{}); ok {
- if i, ok := list[0].(uint16); ok && i != 1 {
+ if i, ok := list[0].(int64); ok && i != 1 {
t.Error("not 1")
}
- if i, ok := list[1].(uint16); ok && i != 2 {
+ if i, ok := list[1].(int64); ok && i != 2 {
t.Error("not 2")
}
- if i, ok := list[2].(uint16); ok && i != 3 {
+ if i, ok := list[2].(int64); ok && i != 3 {
t.Error("not 3")
}
- if i, ok := list[3].(uint16); ok && i != 4 {
+ if i, ok := list[3].(int64); ok && i != 4 {
t.Error("not 4")
}
if i, ok := list[4].(string); ok && i != "hello" {
diff --git a/sys/db/storm/utils.go b/sys/db/storm/utils.go
index eaaa5d42a..509c93e8e 100644
--- a/sys/db/storm/utils.go
+++ b/sys/db/storm/utils.go
@@ -115,12 +115,16 @@ func defaultMatcher(auth model.Authorization, contains string, params *simpleQue
func publishedMatcher(auth model.Authorization, contains string, params *simpleQuery) []q.Matcher {
matchers := commonMatcher(auth, contains, params)
- matchers = append(matchers, q.And(
- q.Or(
+ var m q.Matcher
+ if auth == nil {
+ m = q.Eq("Published", true)
+ } else {
+ m = q.Or(
q.Eq("Owner", auth.UserID()),
q.Eq("Published", true),
- ),
- ))
+ )
+ }
+ matchers = append(matchers, q.And(m))
return matchers
}
diff --git a/sys/db/storm/utils.go-e b/sys/db/storm/utils.go-e
index eaaa5d42a..509c93e8e 100644
--- a/sys/db/storm/utils.go-e
+++ b/sys/db/storm/utils.go-e
@@ -115,12 +115,16 @@ func defaultMatcher(auth model.Authorization, contains string, params *simpleQue
func publishedMatcher(auth model.Authorization, contains string, params *simpleQuery) []q.Matcher {
matchers := commonMatcher(auth, contains, params)
- matchers = append(matchers, q.And(
- q.Or(
+ var m q.Matcher
+ if auth == nil {
+ m = q.Eq("Published", true)
+ } else {
+ m = q.Or(
q.Eq("Owner", auth.UserID()),
q.Eq("Published", true),
- ),
- ))
+ )
+ }
+ matchers = append(matchers, q.And(m))
return matchers
}
diff --git a/sys/db/storm/workflow.go b/sys/db/storm/workflow.go
index 34b099be6..2f17e90e2 100644
--- a/sys/db/storm/workflow.go
+++ b/sys/db/storm/workflow.go
@@ -19,6 +19,7 @@ type WorkflowDBInterface interface {
List(auth model.Authorization, contains string, options map[string]interface{}) ([]*model.WorkflowItem, error)
GetPublished(auth model.Authorization, id string) (*model.WorkflowItem, error)
Get(auth model.Authorization, id string) (*model.WorkflowItem, error)
+ GetList(auth model.Authorization, id []string) ([]*model.WorkflowItem, error)
Put(auth model.Authorization, item *model.WorkflowItem) error
put(auth model.Authorization, item *model.WorkflowItem, updated bool) error
getDB() *storm.DB
@@ -154,6 +155,19 @@ func (me *WorkflowDB) Get(auth model.Authorization, id string) (*model.WorkflowI
return itemRef, nil
}
+// Retrieve multiple workflows. If one is not found, an error is returned
+func (me *WorkflowDB) GetList(auth model.Authorization, ids []string) ([]*model.WorkflowItem, error) {
+ var workflows []*model.WorkflowItem
+ for _, id := range ids {
+ workflow, err := me.Get(auth, id)
+ if err != nil {
+ return nil, err
+ }
+ workflows = append(workflows, workflow)
+ }
+ return workflows, nil
+}
+
func (me *WorkflowDB) Put(auth model.Authorization, item *model.WorkflowItem) error {
return me.put(auth, item, true)
}
diff --git a/sys/db/storm/workflow.go-e b/sys/db/storm/workflow.go-e
index 34b099be6..2f17e90e2 100644
--- a/sys/db/storm/workflow.go-e
+++ b/sys/db/storm/workflow.go-e
@@ -19,6 +19,7 @@ type WorkflowDBInterface interface {
List(auth model.Authorization, contains string, options map[string]interface{}) ([]*model.WorkflowItem, error)
GetPublished(auth model.Authorization, id string) (*model.WorkflowItem, error)
Get(auth model.Authorization, id string) (*model.WorkflowItem, error)
+ GetList(auth model.Authorization, id []string) ([]*model.WorkflowItem, error)
Put(auth model.Authorization, item *model.WorkflowItem) error
put(auth model.Authorization, item *model.WorkflowItem, updated bool) error
getDB() *storm.DB
@@ -154,6 +155,19 @@ func (me *WorkflowDB) Get(auth model.Authorization, id string) (*model.WorkflowI
return itemRef, nil
}
+// Retrieve multiple workflows. If one is not found, an error is returned
+func (me *WorkflowDB) GetList(auth model.Authorization, ids []string) ([]*model.WorkflowItem, error) {
+ var workflows []*model.WorkflowItem
+ for _, id := range ids {
+ workflow, err := me.Get(auth, id)
+ if err != nil {
+ return nil, err
+ }
+ workflows = append(workflows, workflow)
+ }
+ return workflows, nil
+}
+
func (me *WorkflowDB) Put(auth model.Authorization, item *model.WorkflowItem) error {
return me.put(auth, item, true)
}
diff --git a/sys/db/storm/workflow_mock.go b/sys/db/storm/workflow_mock.go
index b7021a696..bbaba688c 100644
--- a/sys/db/storm/workflow_mock.go
+++ b/sys/db/storm/workflow_mock.go
@@ -96,6 +96,21 @@ func (mr *MockWorkflowDBInterfaceMockRecorder) Get(auth, id interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockWorkflowDBInterface)(nil).Get), auth, id)
}
+// GetList mocks base method
+func (m *MockWorkflowDBInterface) GetList(auth model.Authorization, ids []string) ([]*model.WorkflowItem, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetList", auth, ids)
+ ret0, _ := ret[0].([]*model.WorkflowItem)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetList indicates an expected call of GetList
+func (mr *MockWorkflowDBInterfaceMockRecorder) GetList(auth, ids interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetList", reflect.TypeOf((*MockWorkflowDBInterface)(nil).GetList), auth, ids)
+}
+
// Put mocks base method
func (m *MockWorkflowDBInterface) Put(auth model.Authorization, item *model.WorkflowItem) error {
m.ctrl.T.Helper()
diff --git a/sys/db/storm/workflow_mock.go-e b/sys/db/storm/workflow_mock.go-e
index b7021a696..bbaba688c 100644
--- a/sys/db/storm/workflow_mock.go-e
+++ b/sys/db/storm/workflow_mock.go-e
@@ -96,6 +96,21 @@ func (mr *MockWorkflowDBInterfaceMockRecorder) Get(auth, id interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockWorkflowDBInterface)(nil).Get), auth, id)
}
+// GetList mocks base method
+func (m *MockWorkflowDBInterface) GetList(auth model.Authorization, ids []string) ([]*model.WorkflowItem, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetList", auth, ids)
+ ret0, _ := ret[0].([]*model.WorkflowItem)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetList indicates an expected call of GetList
+func (mr *MockWorkflowDBInterfaceMockRecorder) GetList(auth, ids interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetList", reflect.TypeOf((*MockWorkflowDBInterface)(nil).GetList), auth, ids)
+}
+
// Put mocks base method
func (m *MockWorkflowDBInterface) Put(auth model.Authorization, item *model.WorkflowItem) error {
m.ctrl.T.Helper()
diff --git a/sys/db/storm/workflow_payments.go b/sys/db/storm/workflow_payments.go
index 8341dae60..23315566f 100644
--- a/sys/db/storm/workflow_payments.go
+++ b/sys/db/storm/workflow_payments.go
@@ -1,7 +1,11 @@
package storm
import (
+ "errors"
+ "log"
"path/filepath"
+ "strings"
+ "time"
"github.com/asdine/storm"
"github.com/asdine/storm/codec/msgpack"
@@ -11,11 +15,18 @@ import (
)
type WorkflowPaymentsDBInterface interface {
- GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error)
- GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error)
- GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error)
- Add(item *model.WorkflowPaymentItem) error
- Delete(txHash string) error
+ GetByTxHashAndStatusAndFromEthAddress(txHash, status, from string) (*model.WorkflowPaymentItem, error)
+ Get(paymentId string) (*model.WorkflowPaymentItem, error)
+ ConfirmPayment(txHash, from, to string, xes uint64) error
+ GetByWorkflowIdAndFromEthAddress(workflowID, fromEthAddr string, statuses []string) (*model.WorkflowPaymentItem, error)
+ SetAbandonedToTimeoutBeforeTime(beforeTime time.Time) error
+ Save(item *model.WorkflowPaymentItem) error
+ Update(paymentId, status, txHash, from string) error
+ Cancel(paymentId, from string) error
+ Redeem(workflowId, from string) error
+ Delete(paymentId string) error
+ Remove(payment *model.WorkflowPaymentItem) error
+ All() ([]*model.WorkflowPaymentItem, error)
Close() error
}
@@ -23,27 +34,33 @@ type WorkflowPaymentsDB struct {
db *storm.DB
}
-const workflowPaymentVersion = "sig_vers"
-const workflowPaymentDBDir = "workflowpayments"
-const workflowPaymentDB = "workflowpaymentsdb"
+const workflowPaymentVersion = "payment_vers"
+const WorkflowPaymentDBDir = "workflowpayments"
+const WorkflowPaymentDB = "workflowpaymentsdb"
func NewWorkflowPaymentDB(dir string) (*WorkflowPaymentsDB, error) {
var err error
var msgpackDb *storm.DB
- baseDir := filepath.Join(dir, workflowPaymentDBDir)
+ baseDir := filepath.Join(dir, WorkflowPaymentDBDir)
err = ensureDir(baseDir)
if err != nil {
return nil, err
}
- msgpackDb, err = storm.Open(filepath.Join(baseDir, workflowPaymentDB), storm.Codec(msgpack.Codec))
+ msgpackDb, err = storm.Open(filepath.Join(baseDir, WorkflowPaymentDB), storm.Codec(msgpack.Codec))
if err != nil {
return nil, err
}
udb := &WorkflowPaymentsDB{db: msgpackDb}
example := &model.WorkflowPaymentItem{}
- udb.db.Init(example)
- udb.db.ReIndex(example)
+ err = udb.db.Init(example)
+ if err != nil {
+ return nil, err
+ }
+ err = udb.db.ReIndex(example)
+ if err != nil {
+ return nil, err
+ }
err = udb.db.Set(workflowPaymentVersion, workflowPaymentVersion, example.GetVersion())
if err != nil {
@@ -53,72 +70,296 @@ func NewWorkflowPaymentDB(dir string) (*WorkflowPaymentsDB, error) {
return udb, nil
}
-func (me *WorkflowPaymentsDB) GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error) {
- tx, err := me.db.Begin(false)
+func (me *WorkflowPaymentsDB) All() ([]*model.WorkflowPaymentItem, error) {
+ var items []*model.WorkflowPaymentItem
+
+ err := me.db.All(&items)
+ return items, err
+}
+
+func (me *WorkflowPaymentsDB) Get(paymentId string) (*model.WorkflowPaymentItem, error) {
+ var item model.WorkflowPaymentItem
+
+ query := me.db.Select(
+ q.Eq("ID", paymentId),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err := query.First(&item)
+ return &item, err
+}
+func (me *WorkflowPaymentsDB) GetByTxHashAndStatusAndFromEthAddress(txHash, status,
+ fromEthAddr string) (*model.WorkflowPaymentItem, error) {
+
+ var item model.WorkflowPaymentItem
+
+ query := me.db.Select(
+ q.Eq("TxHash", txHash),
+ q.Eq("Status", status),
+ q.Eq("From", fromEthAddr),
+ ).OrderBy("CreatedAt").Reverse()
+
+ err := query.First(&item)
if err != nil {
return nil, err
}
- var item model.WorkflowPaymentItem
- defer tx.Rollback()
- err = tx.One("TxHash", txHash, &item)
+
+ return &item, nil
+}
+
+func (me *WorkflowPaymentsDB) GetByWorkflowIdAndFromEthAddress(workflowID, fromEthAddr string,
+ statuses []string) (*model.WorkflowPaymentItem, error) {
+
+ var (
+ item model.WorkflowPaymentItem
+ query storm.Query
+ )
+
+ if len(statuses) == 0 {
+ query = me.db.Select(
+ q.Eq("WorkflowID", workflowID),
+ q.Eq("From", fromEthAddr),
+ )
+ } else {
+ query = me.db.Select(
+ q.Eq("WorkflowID", workflowID),
+ q.Eq("From", fromEthAddr),
+ q.In("Status", statuses),
+ )
+ }
+
+ query.OrderBy("CreatedAt").Reverse()
+
+ err := query.First(&item)
if err != nil {
return nil, err
}
return &item, nil
}
-func (me *WorkflowPaymentsDB) GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error) {
- tx, err := me.db.Begin(false)
+func (me *WorkflowPaymentsDB) SetAbandonedToTimeoutBeforeTime(beforeTime time.Time) error {
+ query := me.db.Select(
+ q.Or(
+ q.Eq("Status", model.PaymentStatusCreated),
+ q.Eq("Status", model.PaymentStatusPending),
+ ),
+ q.Lt("CreatedAt", beforeTime),
+ )
+
+ return query.Each(new(model.WorkflowPaymentItem), func(record interface{}) error {
+ u := record.(*model.WorkflowPaymentItem)
+ u.Status = model.PaymentStatusTimeout
+ return me.Save(u)
+ })
+}
+
+func (me *WorkflowPaymentsDB) Save(item *model.WorkflowPaymentItem) error {
+ if item.CreatedAt.IsZero() {
+ item.CreatedAt = time.Now()
+ }
+ return me.db.Save(item)
+}
+
+func (me *WorkflowPaymentsDB) ConfirmPayment(txHash, from, to string, xes uint64) error {
+ tx, err := me.db.Begin(true)
if err != nil {
- return nil, err
+ return err
}
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+
var item model.WorkflowPaymentItem
- defer tx.Rollback()
- err = tx.One("WorkflowID", workflowID, &item)
+
+ // Initially try to get payment by TxHash
+ query := tx.Select(
+ q.Eq("TxHash", txHash),
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.In("Status", []string{model.PaymentStatusPending, model.PaymentStatusCreated}),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
if err != nil {
- return nil, err
+ if err != storm.ErrNotFound {
+ return err
+ }
+
+ // prioritize PaymentStatusPending over PaymentStatusCreated
+ query := tx.Select(
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.Eq("Status", model.PaymentStatusPending),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ if err != storm.ErrNotFound {
+ return err
+ }
+
+ query = tx.Select(
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ return err
+ }
+ }
}
- return &item, nil
+
+ item.Status = model.PaymentStatusConfirmed
+ if item.TxHash == "" {
+ item.TxHash = txHash
+ }
+
+ err = tx.Update(&item)
+ if err != nil {
+ log.Println("[ConfirmPayment] tx.Update err: ", err.Error())
+ return err
+ }
+
+ return tx.Commit()
}
-func (me *WorkflowPaymentsDB) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error) {
+func (me *WorkflowPaymentsDB) Redeem(workflowId, from string) error {
+ tx, err := me.db.Begin(true)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+
var item model.WorkflowPaymentItem
- query := me.db.Select(q.Eq("WorkflowID", workflowID), q.Eq("From", ethAddr))
+ query := tx.Select(
+ q.Eq("WorkflowID", workflowId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusConfirmed),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
- err := query.First(&item)
+ err = query.First(&item)
if err != nil {
- return nil, err
+ return err
}
- return &item, nil
+
+ item.Status = model.PaymentStatusRedeemed
+
+ err = tx.Update(&item)
+ if err != nil {
+ return err
+ }
+ return tx.Commit()
+}
+
+func (me *WorkflowPaymentsDB) Cancel(paymentId, from string) error {
+ tx, err := me.db.Begin(true)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+ var item model.WorkflowPaymentItem
+
+ query := tx.Select(
+ q.Eq("ID", paymentId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse() //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ return err
+ }
+
+ item.Status = model.PaymentStatusCancelled
+
+ err = tx.Update(&item)
+ if err != nil {
+ return err
+ }
+ return tx.Commit()
}
-func (me *WorkflowPaymentsDB) Add(item *model.WorkflowPaymentItem) error {
+func (me *WorkflowPaymentsDB) Delete(paymentId string) error {
tx, err := me.db.Begin(true)
if err != nil {
return err
}
- defer tx.Rollback()
- tx.Save(item)
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+ var item model.WorkflowPaymentItem
+
+ err = tx.One("ID", paymentId, &item)
+ if err != nil {
+ return err
+ }
+
+ item.Status = model.PaymentStatusDeleted
+
+ err = tx.Update(&item)
if err != nil {
return err
}
return tx.Commit()
}
-func (me *WorkflowPaymentsDB) Delete(txHash string) error {
+func (me *WorkflowPaymentsDB) Remove(payment *model.WorkflowPaymentItem) error {
+ return me.db.DeleteStruct(payment)
+}
+
+var errNothingToUpdate = errors.New("nothing to update")
+
+func (me *WorkflowPaymentsDB) Update(paymentId, status, txHash, from string) error {
tx, err := me.db.Begin(true)
if err != nil {
return err
}
- defer tx.Rollback()
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
var item model.WorkflowPaymentItem
- err = tx.One("TxHash", txHash, &item)
+ query := tx.Select(
+ q.Eq("ID", paymentId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
if err != nil {
return err
}
- err = tx.DeleteStruct(&item)
+ if strings.TrimSpace(status) == "" && strings.TrimSpace(txHash) == "" {
+ return errNothingToUpdate
+ }
+
+ if strings.TrimSpace(status) != "" {
+ item.Status = status
+ }
+ if strings.TrimSpace(txHash) != "" {
+ item.TxHash = txHash
+ }
+
+ err = tx.Update(&item)
if err != nil {
return err
}
diff --git a/sys/db/storm/workflow_payments.go-e b/sys/db/storm/workflow_payments.go-e
index 8341dae60..23315566f 100644
--- a/sys/db/storm/workflow_payments.go-e
+++ b/sys/db/storm/workflow_payments.go-e
@@ -1,7 +1,11 @@
package storm
import (
+ "errors"
+ "log"
"path/filepath"
+ "strings"
+ "time"
"github.com/asdine/storm"
"github.com/asdine/storm/codec/msgpack"
@@ -11,11 +15,18 @@ import (
)
type WorkflowPaymentsDBInterface interface {
- GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error)
- GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error)
- GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error)
- Add(item *model.WorkflowPaymentItem) error
- Delete(txHash string) error
+ GetByTxHashAndStatusAndFromEthAddress(txHash, status, from string) (*model.WorkflowPaymentItem, error)
+ Get(paymentId string) (*model.WorkflowPaymentItem, error)
+ ConfirmPayment(txHash, from, to string, xes uint64) error
+ GetByWorkflowIdAndFromEthAddress(workflowID, fromEthAddr string, statuses []string) (*model.WorkflowPaymentItem, error)
+ SetAbandonedToTimeoutBeforeTime(beforeTime time.Time) error
+ Save(item *model.WorkflowPaymentItem) error
+ Update(paymentId, status, txHash, from string) error
+ Cancel(paymentId, from string) error
+ Redeem(workflowId, from string) error
+ Delete(paymentId string) error
+ Remove(payment *model.WorkflowPaymentItem) error
+ All() ([]*model.WorkflowPaymentItem, error)
Close() error
}
@@ -23,27 +34,33 @@ type WorkflowPaymentsDB struct {
db *storm.DB
}
-const workflowPaymentVersion = "sig_vers"
-const workflowPaymentDBDir = "workflowpayments"
-const workflowPaymentDB = "workflowpaymentsdb"
+const workflowPaymentVersion = "payment_vers"
+const WorkflowPaymentDBDir = "workflowpayments"
+const WorkflowPaymentDB = "workflowpaymentsdb"
func NewWorkflowPaymentDB(dir string) (*WorkflowPaymentsDB, error) {
var err error
var msgpackDb *storm.DB
- baseDir := filepath.Join(dir, workflowPaymentDBDir)
+ baseDir := filepath.Join(dir, WorkflowPaymentDBDir)
err = ensureDir(baseDir)
if err != nil {
return nil, err
}
- msgpackDb, err = storm.Open(filepath.Join(baseDir, workflowPaymentDB), storm.Codec(msgpack.Codec))
+ msgpackDb, err = storm.Open(filepath.Join(baseDir, WorkflowPaymentDB), storm.Codec(msgpack.Codec))
if err != nil {
return nil, err
}
udb := &WorkflowPaymentsDB{db: msgpackDb}
example := &model.WorkflowPaymentItem{}
- udb.db.Init(example)
- udb.db.ReIndex(example)
+ err = udb.db.Init(example)
+ if err != nil {
+ return nil, err
+ }
+ err = udb.db.ReIndex(example)
+ if err != nil {
+ return nil, err
+ }
err = udb.db.Set(workflowPaymentVersion, workflowPaymentVersion, example.GetVersion())
if err != nil {
@@ -53,72 +70,296 @@ func NewWorkflowPaymentDB(dir string) (*WorkflowPaymentsDB, error) {
return udb, nil
}
-func (me *WorkflowPaymentsDB) GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error) {
- tx, err := me.db.Begin(false)
+func (me *WorkflowPaymentsDB) All() ([]*model.WorkflowPaymentItem, error) {
+ var items []*model.WorkflowPaymentItem
+
+ err := me.db.All(&items)
+ return items, err
+}
+
+func (me *WorkflowPaymentsDB) Get(paymentId string) (*model.WorkflowPaymentItem, error) {
+ var item model.WorkflowPaymentItem
+
+ query := me.db.Select(
+ q.Eq("ID", paymentId),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err := query.First(&item)
+ return &item, err
+}
+func (me *WorkflowPaymentsDB) GetByTxHashAndStatusAndFromEthAddress(txHash, status,
+ fromEthAddr string) (*model.WorkflowPaymentItem, error) {
+
+ var item model.WorkflowPaymentItem
+
+ query := me.db.Select(
+ q.Eq("TxHash", txHash),
+ q.Eq("Status", status),
+ q.Eq("From", fromEthAddr),
+ ).OrderBy("CreatedAt").Reverse()
+
+ err := query.First(&item)
if err != nil {
return nil, err
}
- var item model.WorkflowPaymentItem
- defer tx.Rollback()
- err = tx.One("TxHash", txHash, &item)
+
+ return &item, nil
+}
+
+func (me *WorkflowPaymentsDB) GetByWorkflowIdAndFromEthAddress(workflowID, fromEthAddr string,
+ statuses []string) (*model.WorkflowPaymentItem, error) {
+
+ var (
+ item model.WorkflowPaymentItem
+ query storm.Query
+ )
+
+ if len(statuses) == 0 {
+ query = me.db.Select(
+ q.Eq("WorkflowID", workflowID),
+ q.Eq("From", fromEthAddr),
+ )
+ } else {
+ query = me.db.Select(
+ q.Eq("WorkflowID", workflowID),
+ q.Eq("From", fromEthAddr),
+ q.In("Status", statuses),
+ )
+ }
+
+ query.OrderBy("CreatedAt").Reverse()
+
+ err := query.First(&item)
if err != nil {
return nil, err
}
return &item, nil
}
-func (me *WorkflowPaymentsDB) GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error) {
- tx, err := me.db.Begin(false)
+func (me *WorkflowPaymentsDB) SetAbandonedToTimeoutBeforeTime(beforeTime time.Time) error {
+ query := me.db.Select(
+ q.Or(
+ q.Eq("Status", model.PaymentStatusCreated),
+ q.Eq("Status", model.PaymentStatusPending),
+ ),
+ q.Lt("CreatedAt", beforeTime),
+ )
+
+ return query.Each(new(model.WorkflowPaymentItem), func(record interface{}) error {
+ u := record.(*model.WorkflowPaymentItem)
+ u.Status = model.PaymentStatusTimeout
+ return me.Save(u)
+ })
+}
+
+func (me *WorkflowPaymentsDB) Save(item *model.WorkflowPaymentItem) error {
+ if item.CreatedAt.IsZero() {
+ item.CreatedAt = time.Now()
+ }
+ return me.db.Save(item)
+}
+
+func (me *WorkflowPaymentsDB) ConfirmPayment(txHash, from, to string, xes uint64) error {
+ tx, err := me.db.Begin(true)
if err != nil {
- return nil, err
+ return err
}
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+
var item model.WorkflowPaymentItem
- defer tx.Rollback()
- err = tx.One("WorkflowID", workflowID, &item)
+
+ // Initially try to get payment by TxHash
+ query := tx.Select(
+ q.Eq("TxHash", txHash),
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.In("Status", []string{model.PaymentStatusPending, model.PaymentStatusCreated}),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
if err != nil {
- return nil, err
+ if err != storm.ErrNotFound {
+ return err
+ }
+
+ // prioritize PaymentStatusPending over PaymentStatusCreated
+ query := tx.Select(
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.Eq("Status", model.PaymentStatusPending),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ if err != storm.ErrNotFound {
+ return err
+ }
+
+ query = tx.Select(
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ return err
+ }
+ }
}
- return &item, nil
+
+ item.Status = model.PaymentStatusConfirmed
+ if item.TxHash == "" {
+ item.TxHash = txHash
+ }
+
+ err = tx.Update(&item)
+ if err != nil {
+ log.Println("[ConfirmPayment] tx.Update err: ", err.Error())
+ return err
+ }
+
+ return tx.Commit()
}
-func (me *WorkflowPaymentsDB) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error) {
+func (me *WorkflowPaymentsDB) Redeem(workflowId, from string) error {
+ tx, err := me.db.Begin(true)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+
var item model.WorkflowPaymentItem
- query := me.db.Select(q.Eq("WorkflowID", workflowID), q.Eq("From", ethAddr))
+ query := tx.Select(
+ q.Eq("WorkflowID", workflowId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusConfirmed),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
- err := query.First(&item)
+ err = query.First(&item)
if err != nil {
- return nil, err
+ return err
}
- return &item, nil
+
+ item.Status = model.PaymentStatusRedeemed
+
+ err = tx.Update(&item)
+ if err != nil {
+ return err
+ }
+ return tx.Commit()
+}
+
+func (me *WorkflowPaymentsDB) Cancel(paymentId, from string) error {
+ tx, err := me.db.Begin(true)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+ var item model.WorkflowPaymentItem
+
+ query := tx.Select(
+ q.Eq("ID", paymentId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse() //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ return err
+ }
+
+ item.Status = model.PaymentStatusCancelled
+
+ err = tx.Update(&item)
+ if err != nil {
+ return err
+ }
+ return tx.Commit()
}
-func (me *WorkflowPaymentsDB) Add(item *model.WorkflowPaymentItem) error {
+func (me *WorkflowPaymentsDB) Delete(paymentId string) error {
tx, err := me.db.Begin(true)
if err != nil {
return err
}
- defer tx.Rollback()
- tx.Save(item)
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+ var item model.WorkflowPaymentItem
+
+ err = tx.One("ID", paymentId, &item)
+ if err != nil {
+ return err
+ }
+
+ item.Status = model.PaymentStatusDeleted
+
+ err = tx.Update(&item)
if err != nil {
return err
}
return tx.Commit()
}
-func (me *WorkflowPaymentsDB) Delete(txHash string) error {
+func (me *WorkflowPaymentsDB) Remove(payment *model.WorkflowPaymentItem) error {
+ return me.db.DeleteStruct(payment)
+}
+
+var errNothingToUpdate = errors.New("nothing to update")
+
+func (me *WorkflowPaymentsDB) Update(paymentId, status, txHash, from string) error {
tx, err := me.db.Begin(true)
if err != nil {
return err
}
- defer tx.Rollback()
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
var item model.WorkflowPaymentItem
- err = tx.One("TxHash", txHash, &item)
+ query := tx.Select(
+ q.Eq("ID", paymentId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
if err != nil {
return err
}
- err = tx.DeleteStruct(&item)
+ if strings.TrimSpace(status) == "" && strings.TrimSpace(txHash) == "" {
+ return errNothingToUpdate
+ }
+
+ if strings.TrimSpace(status) != "" {
+ item.Status = status
+ }
+ if strings.TrimSpace(txHash) != "" {
+ item.TxHash = txHash
+ }
+
+ err = tx.Update(&item)
if err != nil {
return err
}
diff --git a/sys/db/storm/workflow_payments_mock.go b/sys/db/storm/workflow_payments_mock.go
index ccdfbc6b1..7c5062ac6 100644
--- a/sys/db/storm/workflow_payments_mock.go
+++ b/sys/db/storm/workflow_payments_mock.go
@@ -35,77 +35,148 @@ func (m *MockWorkflowPaymentsDBInterface) EXPECT() *MockWorkflowPaymentsDBInterf
return m.recorder
}
-// GetByTxHash mocks base method
+// GetByTxHashAndStatusAndFromEthAddress mocks base method
func (m *MockWorkflowPaymentsDBInterface) GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByTxHash", txHash)
+ ret := m.ctrl.Call(m, "GetByTxHashAndStatusAndFromEthAddress", txHash)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetByTxHash indicates an expected call of GetByTxHash
+// GetByTxHashAndStatusAndFromEthAddress indicates an expected call of GetByTxHashAndStatusAndFromEthAddress
func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByTxHash(txHash interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByTxHash", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByTxHash), txHash)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByTxHashAndStatusAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByTxHash), txHash)
}
-// GetByWorkflowId mocks base method
-func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error) {
+// Get mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Get(paymentId, from string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByWorkflowId", workflowID)
+ ret := m.ctrl.Call(m, "Get", paymentId, from)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetByWorkflowId indicates an expected call of GetByWorkflowId
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowId(workflowID interface{}) *gomock.Call {
+// Get indicates an expected call of Get
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Get(paymentId, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowId", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowId), workflowID)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Get), paymentId, from)
+}
+
+// ConfirmPayment mocks base method
+func (m *MockWorkflowPaymentsDBInterface) ConfirmPayment(txHash, from, to string, xes uint64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ConfirmPayment", txHash, from, to, xes)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ConfirmPayment indicates an expected call of ConfirmPayment
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) ConfirmPayment(txHash, from, to, xes interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmPayment", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).ConfirmPayment), txHash, from, to, xes)
}
// GetByWorkflowIdAndFromEthAddress mocks base method
-func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error) {
+func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string, statuses []string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByWorkflowIdAndFromEthAddress", workflowID, ethAddr)
+ ret := m.ctrl.Call(m, "GetByWorkflowIdAndFromEthAddress", workflowID, ethAddr, statuses)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetByWorkflowIdAndFromEthAddress indicates an expected call of GetByWorkflowIdAndFromEthAddress
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr interface{}) *gomock.Call {
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr, statuses interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowIdAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowIdAndFromEthAddress), workflowID, ethAddr, statuses)
+}
+
+// Save mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Save(item *model.WorkflowPaymentItem) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Save", item)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Save indicates an expected call of Save
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Save(item interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Save), item)
+}
+
+// Update mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Update(paymentId, status, txHash, from string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Update", paymentId, status, txHash, from)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Update indicates an expected call of Update
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Update(paymentId, status, txHash, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowIdAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowIdAndFromEthAddress), workflowID, ethAddr)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Update), paymentId, status, txHash, from)
}
-// Add mocks base method
-func (m *MockWorkflowPaymentsDBInterface) Add(item *model.WorkflowPaymentItem) error {
+// Cancel mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Cancel(paymentId, from string) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Add", item)
+ ret := m.ctrl.Call(m, "Cancel", paymentId, from)
ret0, _ := ret[0].(error)
return ret0
}
-// Add indicates an expected call of Add
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Add(item interface{}) *gomock.Call {
+// Cancel indicates an expected call of Cancel
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Cancel(paymentId, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Add), item)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Cancel), paymentId, from)
}
// Delete mocks base method
-func (m *MockWorkflowPaymentsDBInterface) Delete(txHash string) error {
+func (m *MockWorkflowPaymentsDBInterface) Delete(paymentId string) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Delete", txHash)
+ ret := m.ctrl.Call(m, "Delete", paymentId)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Delete(txHash interface{}) *gomock.Call {
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Delete(paymentId interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Delete), paymentId)
+}
+
+// Redeem mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Redeem(workflowId, from string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Redeem", workflowId, from)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Redeem indicates an expected call of Redeem
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Redeem(workflowId, from interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Redeem", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Redeem), workflowId, from)
+}
+
+// All mocks base method
+func (m *MockWorkflowPaymentsDBInterface) All() ([]*model.WorkflowPaymentItem, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "All")
+ ret0, _ := ret[0].([]*model.WorkflowPaymentItem)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// All indicates an expected call of All
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) All() *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Delete), txHash)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).All))
}
// Close mocks base method
diff --git a/sys/db/storm/workflow_payments_mock.go-e b/sys/db/storm/workflow_payments_mock.go-e
index ccdfbc6b1..7c5062ac6 100644
--- a/sys/db/storm/workflow_payments_mock.go-e
+++ b/sys/db/storm/workflow_payments_mock.go-e
@@ -35,77 +35,148 @@ func (m *MockWorkflowPaymentsDBInterface) EXPECT() *MockWorkflowPaymentsDBInterf
return m.recorder
}
-// GetByTxHash mocks base method
+// GetByTxHashAndStatusAndFromEthAddress mocks base method
func (m *MockWorkflowPaymentsDBInterface) GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByTxHash", txHash)
+ ret := m.ctrl.Call(m, "GetByTxHashAndStatusAndFromEthAddress", txHash)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetByTxHash indicates an expected call of GetByTxHash
+// GetByTxHashAndStatusAndFromEthAddress indicates an expected call of GetByTxHashAndStatusAndFromEthAddress
func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByTxHash(txHash interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByTxHash", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByTxHash), txHash)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByTxHashAndStatusAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByTxHash), txHash)
}
-// GetByWorkflowId mocks base method
-func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error) {
+// Get mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Get(paymentId, from string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByWorkflowId", workflowID)
+ ret := m.ctrl.Call(m, "Get", paymentId, from)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetByWorkflowId indicates an expected call of GetByWorkflowId
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowId(workflowID interface{}) *gomock.Call {
+// Get indicates an expected call of Get
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Get(paymentId, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowId", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowId), workflowID)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Get), paymentId, from)
+}
+
+// ConfirmPayment mocks base method
+func (m *MockWorkflowPaymentsDBInterface) ConfirmPayment(txHash, from, to string, xes uint64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ConfirmPayment", txHash, from, to, xes)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ConfirmPayment indicates an expected call of ConfirmPayment
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) ConfirmPayment(txHash, from, to, xes interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmPayment", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).ConfirmPayment), txHash, from, to, xes)
}
// GetByWorkflowIdAndFromEthAddress mocks base method
-func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error) {
+func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string, statuses []string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByWorkflowIdAndFromEthAddress", workflowID, ethAddr)
+ ret := m.ctrl.Call(m, "GetByWorkflowIdAndFromEthAddress", workflowID, ethAddr, statuses)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetByWorkflowIdAndFromEthAddress indicates an expected call of GetByWorkflowIdAndFromEthAddress
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr interface{}) *gomock.Call {
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr, statuses interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowIdAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowIdAndFromEthAddress), workflowID, ethAddr, statuses)
+}
+
+// Save mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Save(item *model.WorkflowPaymentItem) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Save", item)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Save indicates an expected call of Save
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Save(item interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Save), item)
+}
+
+// Update mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Update(paymentId, status, txHash, from string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Update", paymentId, status, txHash, from)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Update indicates an expected call of Update
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Update(paymentId, status, txHash, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowIdAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowIdAndFromEthAddress), workflowID, ethAddr)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Update), paymentId, status, txHash, from)
}
-// Add mocks base method
-func (m *MockWorkflowPaymentsDBInterface) Add(item *model.WorkflowPaymentItem) error {
+// Cancel mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Cancel(paymentId, from string) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Add", item)
+ ret := m.ctrl.Call(m, "Cancel", paymentId, from)
ret0, _ := ret[0].(error)
return ret0
}
-// Add indicates an expected call of Add
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Add(item interface{}) *gomock.Call {
+// Cancel indicates an expected call of Cancel
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Cancel(paymentId, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Add), item)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Cancel), paymentId, from)
}
// Delete mocks base method
-func (m *MockWorkflowPaymentsDBInterface) Delete(txHash string) error {
+func (m *MockWorkflowPaymentsDBInterface) Delete(paymentId string) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Delete", txHash)
+ ret := m.ctrl.Call(m, "Delete", paymentId)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Delete(txHash interface{}) *gomock.Call {
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Delete(paymentId interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Delete), paymentId)
+}
+
+// Redeem mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Redeem(workflowId, from string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Redeem", workflowId, from)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Redeem indicates an expected call of Redeem
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Redeem(workflowId, from interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Redeem", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Redeem), workflowId, from)
+}
+
+// All mocks base method
+func (m *MockWorkflowPaymentsDBInterface) All() ([]*model.WorkflowPaymentItem, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "All")
+ ret0, _ := ret[0].([]*model.WorkflowPaymentItem)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// All indicates an expected call of All
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) All() *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Delete), txHash)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).All))
}
// Close mocks base method
diff --git a/sys/db_test.go b/sys/db_test.go
index 1d4f4f6ff..59d32caf8 100644
--- a/sys/db_test.go
+++ b/sys/db_test.go
@@ -38,7 +38,7 @@ func TestRemWildcards(t *testing.T) {
}
func TestUnifySelector(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion2.db"
db, err := Open(dbPath)
sel, keys := db.unify(`omg.blabla["myemail@gmail.com"].omg.blabla[1]`)
fmt.Println(string(*sel))
@@ -52,7 +52,7 @@ func TestUnifySelector(t *testing.T) {
}
func TestAbsoluteKeyPath(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion3.db"
db, err := Open(dbPath)
var config = map[string]interface{}{"absolute.key.path": true, "sort": map[string]interface{}{"firstObject": "asc"}}
_ = db.Write(map[string]interface{}{
@@ -94,7 +94,7 @@ func TestArray(t *testing.T) {
}
func TestDelete(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion4.db"
db, err := Open(dbPath)
var i int = 2147483647
_ = db.Write(map[string]interface{}{
@@ -122,7 +122,7 @@ func TestDelete(t *testing.T) {
}
func TestWriteReadTypeConversion(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion5.db"
db, err := Open(dbPath)
var i int = 2147483647
var imin int = -2147483648
diff --git a/sys/db_test.go-e b/sys/db_test.go-e
index 1d4f4f6ff..59d32caf8 100644
--- a/sys/db_test.go-e
+++ b/sys/db_test.go-e
@@ -38,7 +38,7 @@ func TestRemWildcards(t *testing.T) {
}
func TestUnifySelector(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion2.db"
db, err := Open(dbPath)
sel, keys := db.unify(`omg.blabla["myemail@gmail.com"].omg.blabla[1]`)
fmt.Println(string(*sel))
@@ -52,7 +52,7 @@ func TestUnifySelector(t *testing.T) {
}
func TestAbsoluteKeyPath(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion3.db"
db, err := Open(dbPath)
var config = map[string]interface{}{"absolute.key.path": true, "sort": map[string]interface{}{"firstObject": "asc"}}
_ = db.Write(map[string]interface{}{
@@ -94,7 +94,7 @@ func TestArray(t *testing.T) {
}
func TestDelete(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion4.db"
db, err := Open(dbPath)
var i int = 2147483647
_ = db.Write(map[string]interface{}{
@@ -122,7 +122,7 @@ func TestDelete(t *testing.T) {
}
func TestWriteReadTypeConversion(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion5.db"
db, err := Open(dbPath)
var i int = 2147483647
var imin int = -2147483648
diff --git a/sys/model/all.go b/sys/model/all.go
index 27f39d100..2fa51f3d8 100644
--- a/sys/model/all.go
+++ b/sys/model/all.go
@@ -12,7 +12,10 @@ const (
PaymentStatusCreated = "created"
PaymentStatusPending = "pending"
PaymentStatusConfirmed = "confirmed"
- PaymentStatusFinished = "finished"
+ PaymentStatusCancelled = "cancelled"
+ PaymentStatusRedeemed = "redeemed"
+ PaymentStatusDeleted = "deleted"
+ PaymentStatusTimeout = "timeout"
)
type (
@@ -111,12 +114,13 @@ type (
WorkflowPaymentItem struct {
//save from who payment
- TxHash string `json:"hash" storm:"id"`
+ ID string `json:"id" storm:"id,unique"`
+ TxHash string `json:"hash" storm:"index,unique"`
WorkflowID string `json:"workflowID" storm:"index"`
- From string `json:"From"`
- To string `json:"To"`
+ From string `json:"from" storm:"index"`
+ To string `json:"to"`
Xes uint64 `json:"xes"`
- Status string `json:"Status"`
+ Status string `json:"status" storm:"index"`
CreatedAt time.Time `json:"createdAt"`
}
@@ -127,6 +131,12 @@ func (me *FormItem) GetVersion() int {
return 0
}
+func (me *FormItem) Clone() FormItem {
+ form := *me
+ form.ID = ""
+ return form
+}
+
func (me *FormComponentItem) GetVersion() int {
return 0
}
@@ -135,6 +145,18 @@ func (me *WorkflowItem) GetVersion() int {
return 0
}
+func (me *WorkflowItem) Clone() WorkflowItem {
+ workflow := *me
+ workflow.ID = "" // without id the repository will create a new one
+ return workflow
+}
+
+func (me *TemplateItem) Clone() TemplateItem {
+ template := *me
+ template.ID = ""
+ return template
+}
+
func (me *TemplateItem) GetVersion() int {
return 1
}
diff --git a/sys/model/all.go-e b/sys/model/all.go-e
index 27f39d100..2fa51f3d8 100644
--- a/sys/model/all.go-e
+++ b/sys/model/all.go-e
@@ -12,7 +12,10 @@ const (
PaymentStatusCreated = "created"
PaymentStatusPending = "pending"
PaymentStatusConfirmed = "confirmed"
- PaymentStatusFinished = "finished"
+ PaymentStatusCancelled = "cancelled"
+ PaymentStatusRedeemed = "redeemed"
+ PaymentStatusDeleted = "deleted"
+ PaymentStatusTimeout = "timeout"
)
type (
@@ -111,12 +114,13 @@ type (
WorkflowPaymentItem struct {
//save from who payment
- TxHash string `json:"hash" storm:"id"`
+ ID string `json:"id" storm:"id,unique"`
+ TxHash string `json:"hash" storm:"index,unique"`
WorkflowID string `json:"workflowID" storm:"index"`
- From string `json:"From"`
- To string `json:"To"`
+ From string `json:"from" storm:"index"`
+ To string `json:"to"`
Xes uint64 `json:"xes"`
- Status string `json:"Status"`
+ Status string `json:"status" storm:"index"`
CreatedAt time.Time `json:"createdAt"`
}
@@ -127,6 +131,12 @@ func (me *FormItem) GetVersion() int {
return 0
}
+func (me *FormItem) Clone() FormItem {
+ form := *me
+ form.ID = ""
+ return form
+}
+
func (me *FormComponentItem) GetVersion() int {
return 0
}
@@ -135,6 +145,18 @@ func (me *WorkflowItem) GetVersion() int {
return 0
}
+func (me *WorkflowItem) Clone() WorkflowItem {
+ workflow := *me
+ workflow.ID = "" // without id the repository will create a new one
+ return workflow
+}
+
+func (me *TemplateItem) Clone() TemplateItem {
+ template := *me
+ template.ID = ""
+ return template
+}
+
func (me *TemplateItem) GetVersion() int {
return 1
}
diff --git a/sys/model/settings.go b/sys/model/settings.go
index 35e7d40a0..77d96e70c 100644
--- a/sys/model/settings.go
+++ b/sys/model/settings.go
@@ -15,6 +15,11 @@ type Settings struct {
SparkpostApiKey string `json:"sparkpostApiKey" validate:"required=true" usage:"Sparkpost API key which will be used to send out emails."`
EmailFrom string `json:"emailFrom" validate:"required=true,email=true" usage:"Email that is being used to send out emails."`
LogPath string `json:"logPath" default:"./log" usage:"Location of the log file of this service."`
+ DefaultWorkflowIds string `json:"defaultWorkflowIds" usage:"Workflow IDs to set to clone and add to a new user"`
+ AirdropEnabled string `json:"airdropEnabled" validate:"required=true" default:"false" usage:"Enables/Disables the XES & Ether airdrop feature on ropsten."`
+ AirdropAmountXES string `json:"airdropAmountXES" default:"0" usage:"Amount of XES to airdrop to newly registered users."`
+ AirdropAmountEther string `json:"airdropAmountEther" default:"0" usage:"Amount of Ether to airdrop to newly registered users."`
+ TestMode string `json:"testMode" default:"false" usage:"Run the server in test mode (NOT FOR PRODUCTION)."`
}
func NewDefaultSettings() *Settings {
diff --git a/sys/model/settings.go-e b/sys/model/settings.go-e
index 35e7d40a0..77d96e70c 100644
--- a/sys/model/settings.go-e
+++ b/sys/model/settings.go-e
@@ -15,6 +15,11 @@ type Settings struct {
SparkpostApiKey string `json:"sparkpostApiKey" validate:"required=true" usage:"Sparkpost API key which will be used to send out emails."`
EmailFrom string `json:"emailFrom" validate:"required=true,email=true" usage:"Email that is being used to send out emails."`
LogPath string `json:"logPath" default:"./log" usage:"Location of the log file of this service."`
+ DefaultWorkflowIds string `json:"defaultWorkflowIds" usage:"Workflow IDs to set to clone and add to a new user"`
+ AirdropEnabled string `json:"airdropEnabled" validate:"required=true" default:"false" usage:"Enables/Disables the XES & Ether airdrop feature on ropsten."`
+ AirdropAmountXES string `json:"airdropAmountXES" default:"0" usage:"Amount of XES to airdrop to newly registered users."`
+ AirdropAmountEther string `json:"airdropAmountEther" default:"0" usage:"Amount of Ether to airdrop to newly registered users."`
+ TestMode string `json:"testMode" default:"false" usage:"Run the server in test mode (NOT FOR PRODUCTION)."`
}
func NewDefaultSettings() *Settings {
diff --git a/sys/session/manager.go b/sys/session/manager.go
index a316293f8..e2bface1b 100644
--- a/sys/session/manager.go
+++ b/sys/session/manager.go
@@ -184,7 +184,10 @@ func (me *Manager) Clean() error {
func (me *Manager) Close() (err error) {
me.sessionsDB.IterateMemStorage(func(key string, val interface{}) {
if sess, ok := val.(*Session); ok {
- _ = sess.close()
+ err = sess.close()
+ if err != nil {
+ log.Println(err.Error())
+ }
}
})
me.sessionsDB.Close()
diff --git a/sys/session/manager.go-e b/sys/session/manager.go-e
index a316293f8..e2bface1b 100644
--- a/sys/session/manager.go-e
+++ b/sys/session/manager.go-e
@@ -184,7 +184,10 @@ func (me *Manager) Clean() error {
func (me *Manager) Close() (err error) {
me.sessionsDB.IterateMemStorage(func(key string, val interface{}) {
if sess, ok := val.(*Session); ok {
- _ = sess.close()
+ err = sess.close()
+ if err != nil {
+ log.Println(err.Error())
+ }
}
})
me.sessionsDB.Close()
diff --git a/sys/session/manager_test.go b/sys/session/manager_test.go
index 2192cc9b0..eac51e714 100644
--- a/sys/session/manager_test.go
+++ b/sys/session/manager_test.go
@@ -95,7 +95,7 @@ func TestOnCreatedOnLoadOnExpireOnRemove(t *testing.T) {
}
time.Sleep(time.Second * 1)
if exired, exists := myOnExpireMap[s1ID]; !exists || !exired {
- t.Error(s1ID, "not exired")
+ t.Error(s1ID, "not expired")
}
if exired, exists := myOnExpireMap[s2ID]; !exists || !exired {
t.Error(s2ID, "not exired")
@@ -178,8 +178,9 @@ type MySessionObject struct {
closeCalled bool
}
-func (me *MySessionObject) Close() {
+func (me *MySessionObject) Close() error {
me.closeCalled = true
+ return nil
}
type MySessionObject2 struct {
@@ -195,13 +196,13 @@ type MySessionObject3 struct {
closeCalled bool
}
-func (me *MySessionObject3) Close() (string, error) {
+func (me *MySessionObject3) Close() error {
me.closeCalled = true
- return "", nil
+ return nil
}
func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
- sessDir := "./testSessionDir"
+ sessDir := "./testSessionDir2"
expiry := time.Millisecond * 800
myOnCreatedMap := make(map[string]bool)
myOnLoadMap := make(map[string]bool)
@@ -256,11 +257,11 @@ func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
if err != nil || s == nil {
t.Error(err, s)
}
- if exired, exists := myOnExpireMap[s1ID]; exists || exired {
+ if expired, exists := myOnExpireMap[s1ID]; exists || expired {
t.Error(s1ID, "should't exire")
}
time.Sleep(time.Second * 1)
- if exired, exists := myOnExpireMap[s1ID]; !exists || !exired {
+ if expired, exists := myOnExpireMap[s1ID]; !exists || !expired {
t.Error(s1ID, "not exired")
}
if !obj1.closeCalled {
@@ -281,7 +282,7 @@ func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
}
func TestCloseOnSessionMemStoreWhenClosingManager(t *testing.T) {
- sessDir := "./testSessionDir"
+ sessDir := "./testSessionDir3"
expiry := time.Second * 3
sm, err := NewManager(sessDir, expiry)
if err != nil {
diff --git a/sys/session/manager_test.go-e b/sys/session/manager_test.go-e
index 2192cc9b0..eac51e714 100644
--- a/sys/session/manager_test.go-e
+++ b/sys/session/manager_test.go-e
@@ -95,7 +95,7 @@ func TestOnCreatedOnLoadOnExpireOnRemove(t *testing.T) {
}
time.Sleep(time.Second * 1)
if exired, exists := myOnExpireMap[s1ID]; !exists || !exired {
- t.Error(s1ID, "not exired")
+ t.Error(s1ID, "not expired")
}
if exired, exists := myOnExpireMap[s2ID]; !exists || !exired {
t.Error(s2ID, "not exired")
@@ -178,8 +178,9 @@ type MySessionObject struct {
closeCalled bool
}
-func (me *MySessionObject) Close() {
+func (me *MySessionObject) Close() error {
me.closeCalled = true
+ return nil
}
type MySessionObject2 struct {
@@ -195,13 +196,13 @@ type MySessionObject3 struct {
closeCalled bool
}
-func (me *MySessionObject3) Close() (string, error) {
+func (me *MySessionObject3) Close() error {
me.closeCalled = true
- return "", nil
+ return nil
}
func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
- sessDir := "./testSessionDir"
+ sessDir := "./testSessionDir2"
expiry := time.Millisecond * 800
myOnCreatedMap := make(map[string]bool)
myOnLoadMap := make(map[string]bool)
@@ -256,11 +257,11 @@ func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
if err != nil || s == nil {
t.Error(err, s)
}
- if exired, exists := myOnExpireMap[s1ID]; exists || exired {
+ if expired, exists := myOnExpireMap[s1ID]; exists || expired {
t.Error(s1ID, "should't exire")
}
time.Sleep(time.Second * 1)
- if exired, exists := myOnExpireMap[s1ID]; !exists || !exired {
+ if expired, exists := myOnExpireMap[s1ID]; !exists || !expired {
t.Error(s1ID, "not exired")
}
if !obj1.closeCalled {
@@ -281,7 +282,7 @@ func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
}
func TestCloseOnSessionMemStoreWhenClosingManager(t *testing.T) {
- sessDir := "./testSessionDir"
+ sessDir := "./testSessionDir3"
expiry := time.Second * 3
sm, err := NewManager(sessDir, expiry)
if err != nil {
diff --git a/sys/session/session.go b/sys/session/session.go
index 941a5f181..7190abd0d 100644
--- a/sys/session/session.go
+++ b/sys/session/session.go
@@ -230,6 +230,9 @@ func (me *Session) close() (err error) {
me.store.IterateMemStorage(func(key string, val interface{}) {
//ensure all changes during runtime are persisted before closing
me.store.UpdatedValueRef(key)
+ if closer, ok := val.(io.Closer); ok {
+ closer.Close()
+ }
})
me.store.Close()
}
diff --git a/sys/session/session.go-e b/sys/session/session.go-e
index 941a5f181..7190abd0d 100644
--- a/sys/session/session.go-e
+++ b/sys/session/session.go-e
@@ -230,6 +230,9 @@ func (me *Session) close() (err error) {
me.store.IterateMemStorage(func(key string, val interface{}) {
//ensure all changes during runtime are persisted before closing
me.store.UpdatedValueRef(key)
+ if closer, ok := val.(io.Closer); ok {
+ closer.Close()
+ }
})
me.store.Close()
}
diff --git a/sys/system.go b/sys/system.go
index 4b74c3f9c..5f9c32b5a 100644
--- a/sys/system.go
+++ b/sys/system.go
@@ -43,6 +43,7 @@ var (
type (
System struct {
+ TestMode bool
SessionMgmnt *session.Manager
DB *storm.DBSet
DS *eio.DocumentServiceClient
@@ -53,6 +54,7 @@ type (
fallbackSettings *model.Settings
paymentListenerCancelFunc context.CancelFunc
signatureListenerCancelFunc context.CancelFunc
+ tick *time.Ticker
}
sessionNotify struct {
system *System
@@ -93,6 +95,10 @@ func NewWithSettings(settings model.Settings) (*System, error) {
}
me := &System{settingsDB: stngsDB, fallbackSettings: &settings}
+ if strings.ToLower(settings.TestMode) == "true" {
+ me.TestMode = true
+ }
+
err = me.init(me.GetSettings())
if err != nil {
return nil, err
@@ -101,10 +107,9 @@ func NewWithSettings(settings model.Settings) (*System, error) {
}
func (me *System) init(stngs *model.Settings) error {
- log.Println("Init with settings: ", stngs)
- var err error
- var expiry time.Duration
- expiry, err = time.ParseDuration(stngs.SessionExpiry)
+ log.Printf("Init with settings: %#v\n", stngs)
+
+ expiry, err := time.ParseDuration(stngs.SessionExpiry)
if err != nil {
expiry = time.Hour
}
@@ -130,6 +135,10 @@ func (me *System) init(stngs *model.Settings) error {
log.Println("Wrong blockchain network: ", stngs.BlockchainNet)
}
+ cfg.Config.AirdropEnabled = stngs.AirdropEnabled
+ cfg.Config.AirdropAmountEther = stngs.AirdropAmountEther
+ cfg.Config.AirdropAmountXES = stngs.AirdropAmountXES
+
me.closeDBs()
var cacheExpiry time.Duration
cacheExpiry, err = time.ParseDuration(stngs.CacheExpiry)
@@ -201,9 +210,28 @@ func (me *System) init(stngs *model.Settings) error {
me.signatureListenerCancelFunc = cancelSig
go bcListenerSignature.Listen(ctxSig)
+ if me.tick != nil {
+ me.tick.Stop()
+ }
+ me.tick = time.NewTicker(time.Hour * 6)
+ go me.scheduledCleanup(me.tick)
+
return nil
}
+func (me *System) scheduledCleanup(tick *time.Ticker) {
+ for range tick.C {
+ beforeTime := time.Now().AddDate(0, 0, -14)
+ log.Println("[scheduler][workflowpaymentcleanup] Timing out abandoned payments from before ", beforeTime)
+ err := me.DB.WorkflowPaymentsDB.SetAbandonedToTimeoutBeforeTime(beforeTime)
+ if err != nil {
+ log.Println("[scheduler][workflowpaymentcleanup] err: ", err.Error())
+ continue
+ }
+ log.Println("[scheduler][workflowpaymentcleanup] Done")
+ }
+}
+
func (me *System) Configured() (bool, error) {
count, err := me.DB.User.Count()
if err != nil {
diff --git a/sys/system.go-e b/sys/system.go-e
index 4b74c3f9c..5f9c32b5a 100644
--- a/sys/system.go-e
+++ b/sys/system.go-e
@@ -43,6 +43,7 @@ var (
type (
System struct {
+ TestMode bool
SessionMgmnt *session.Manager
DB *storm.DBSet
DS *eio.DocumentServiceClient
@@ -53,6 +54,7 @@ type (
fallbackSettings *model.Settings
paymentListenerCancelFunc context.CancelFunc
signatureListenerCancelFunc context.CancelFunc
+ tick *time.Ticker
}
sessionNotify struct {
system *System
@@ -93,6 +95,10 @@ func NewWithSettings(settings model.Settings) (*System, error) {
}
me := &System{settingsDB: stngsDB, fallbackSettings: &settings}
+ if strings.ToLower(settings.TestMode) == "true" {
+ me.TestMode = true
+ }
+
err = me.init(me.GetSettings())
if err != nil {
return nil, err
@@ -101,10 +107,9 @@ func NewWithSettings(settings model.Settings) (*System, error) {
}
func (me *System) init(stngs *model.Settings) error {
- log.Println("Init with settings: ", stngs)
- var err error
- var expiry time.Duration
- expiry, err = time.ParseDuration(stngs.SessionExpiry)
+ log.Printf("Init with settings: %#v\n", stngs)
+
+ expiry, err := time.ParseDuration(stngs.SessionExpiry)
if err != nil {
expiry = time.Hour
}
@@ -130,6 +135,10 @@ func (me *System) init(stngs *model.Settings) error {
log.Println("Wrong blockchain network: ", stngs.BlockchainNet)
}
+ cfg.Config.AirdropEnabled = stngs.AirdropEnabled
+ cfg.Config.AirdropAmountEther = stngs.AirdropAmountEther
+ cfg.Config.AirdropAmountXES = stngs.AirdropAmountXES
+
me.closeDBs()
var cacheExpiry time.Duration
cacheExpiry, err = time.ParseDuration(stngs.CacheExpiry)
@@ -201,9 +210,28 @@ func (me *System) init(stngs *model.Settings) error {
me.signatureListenerCancelFunc = cancelSig
go bcListenerSignature.Listen(ctxSig)
+ if me.tick != nil {
+ me.tick.Stop()
+ }
+ me.tick = time.NewTicker(time.Hour * 6)
+ go me.scheduledCleanup(me.tick)
+
return nil
}
+func (me *System) scheduledCleanup(tick *time.Ticker) {
+ for range tick.C {
+ beforeTime := time.Now().AddDate(0, 0, -14)
+ log.Println("[scheduler][workflowpaymentcleanup] Timing out abandoned payments from before ", beforeTime)
+ err := me.DB.WorkflowPaymentsDB.SetAbandonedToTimeoutBeforeTime(beforeTime)
+ if err != nil {
+ log.Println("[scheduler][workflowpaymentcleanup] err: ", err.Error())
+ continue
+ }
+ log.Println("[scheduler][workflowpaymentcleanup] Done")
+ }
+}
+
func (me *System) Configured() (bool, error) {
count, err := me.DB.User.Count()
if err != nil {
diff --git a/sys/system_test.go b/sys/system_test.go
index 0758c946a..2c36cc8f6 100644
--- a/sys/system_test.go
+++ b/sys/system_test.go
@@ -16,7 +16,7 @@ func TestNew(t *testing.T) {
wfItem(m)
}
-func wfItem(a model.PermissionItem) {
+func wfItem(a *model.WorkflowItem) {
bts, err := json.Marshal(a)
log.Println(err, string(bts))
}
diff --git a/sys/system_test.go-e b/sys/system_test.go-e
index 0758c946a..2c36cc8f6 100644
--- a/sys/system_test.go-e
+++ b/sys/system_test.go-e
@@ -16,7 +16,7 @@ func TestNew(t *testing.T) {
wfItem(m)
}
-func wfItem(a model.PermissionItem) {
+func wfItem(a *model.WorkflowItem) {
bts, err := json.Marshal(a)
log.Println(err, string(bts))
}
diff --git a/sys/workflow/workflow_test.go b/sys/workflow/workflow_test.go
index b461165e4..3917e755e 100644
--- a/sys/workflow/workflow_test.go
+++ b/sys/workflow/workflow_test.go
@@ -67,7 +67,7 @@ func NewUserImpl(n *Node) (NodeIF, error) {
return &UserImpl{}, nil
}
-func (me *FormImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *FormImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
@@ -75,14 +75,14 @@ func (me *FormImpl) Execute(n *Node, data interface{}) (bool, error) {
if !me.presented {
//present
if testVerbose {
- log.Println("--->WF TEST IMPL [form] Execute present state", n, data)
+ log.Println("--->WF TEST IMPL [form] Execute present state", n)
}
me.presented = true
return false, nil
}
//validate
if testVerbose {
- log.Println("--->WF TEST IMPL [form] Execute validate state", n, data)
+ log.Println("--->WF TEST IMPL [form] Execute validate state", n)
}
return true, nil
}
@@ -101,13 +101,13 @@ func (me *FormImpl) Close() {
me.n = nil
}
-func (me *UserImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *UserImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
nodeStateMap[n.ID] = true
if testVerbose {
- log.Println("--->WF TEST IMPL [user] Execute", n, data)
+ log.Println("--->WF TEST IMPL [user] Execute", n)
}
return true, nil
}
@@ -126,13 +126,13 @@ func (me *UserImpl) Close() {
me.n = nil
}
-func (me *TemplateImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *TemplateImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
nodeStateMap[n.ID] = true
if testVerbose {
- log.Println("--->WF TEST IMPL [template] Execute", n, data)
+ log.Println("--->WF TEST IMPL [template] Execute", n)
}
return true, nil
}
diff --git a/sys/workflow/workflow_test.go-e b/sys/workflow/workflow_test.go-e
index b461165e4..3917e755e 100644
--- a/sys/workflow/workflow_test.go-e
+++ b/sys/workflow/workflow_test.go-e
@@ -67,7 +67,7 @@ func NewUserImpl(n *Node) (NodeIF, error) {
return &UserImpl{}, nil
}
-func (me *FormImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *FormImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
@@ -75,14 +75,14 @@ func (me *FormImpl) Execute(n *Node, data interface{}) (bool, error) {
if !me.presented {
//present
if testVerbose {
- log.Println("--->WF TEST IMPL [form] Execute present state", n, data)
+ log.Println("--->WF TEST IMPL [form] Execute present state", n)
}
me.presented = true
return false, nil
}
//validate
if testVerbose {
- log.Println("--->WF TEST IMPL [form] Execute validate state", n, data)
+ log.Println("--->WF TEST IMPL [form] Execute validate state", n)
}
return true, nil
}
@@ -101,13 +101,13 @@ func (me *FormImpl) Close() {
me.n = nil
}
-func (me *UserImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *UserImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
nodeStateMap[n.ID] = true
if testVerbose {
- log.Println("--->WF TEST IMPL [user] Execute", n, data)
+ log.Println("--->WF TEST IMPL [user] Execute", n)
}
return true, nil
}
@@ -126,13 +126,13 @@ func (me *UserImpl) Close() {
me.n = nil
}
-func (me *TemplateImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *TemplateImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
nodeStateMap[n.ID] = true
if testVerbose {
- log.Println("--->WF TEST IMPL [template] Execute", n, data)
+ log.Println("--->WF TEST IMPL [template] Execute", n)
}
return true, nil
}
diff --git a/test/apikey_test.go b/test/apikey_test.go
new file mode 100644
index 000000000..fdb197ff0
--- /dev/null
+++ b/test/apikey_test.go
@@ -0,0 +1,46 @@
+package test
+
+import (
+ "encoding/base64"
+ "net/http"
+ "testing"
+)
+
+func TestApiKey(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+
+ login(s, u)
+ apiKey, summary := createApiKey(s, u, "test-"+s.id)
+ logout(s)
+
+ token := getSessionToken(s, u.username, apiKey)
+ deleteSessionToken(s, token)
+
+ login(s, u)
+ deleteApiKey(s, u, summary)
+ deleteUser(s, u)
+}
+
+func createApiKey(s *session, u *user, name string) (string, string) {
+ key := s.e.GET("/api/user/create/api/key/{id}").WithPath("id", u.uuid).WithQuery("name", name).Expect().Status(http.StatusOK).Body().Raw()
+
+ summary := key[:4] + "..." + key[len(key)-4:]
+ s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Path("$..Key").Array().Contains(summary)
+
+ return key, summary
+}
+
+func getSessionToken(s *session, username, apiKey string) string {
+ b := base64.StdEncoding.EncodeToString([]byte(username + ":" + apiKey))
+ return s.e.GET("/api/session/token").WithHeader("Authorization", "Basic "+b).Expect().Status(http.StatusOK).JSON().Object().Value("token").String().NotEmpty().Raw()
+}
+
+func deleteSessionToken(s *session, token string) {
+ s.e.DELETE("/api/session/token").WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).NoContent()
+}
+
+func deleteApiKey(s *session, u *user, summary string) {
+ s.e.DELETE("/api/user/create/api/key/{id}").WithPath("id", u.uuid).WithQuery("hiddenApiKey", summary).Expect().Status(http.StatusOK)
+ s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Path("$..Key").Array().NotContains(summary)
+}
diff --git a/test/apikey_test.go-e b/test/apikey_test.go-e
new file mode 100644
index 000000000..fdb197ff0
--- /dev/null
+++ b/test/apikey_test.go-e
@@ -0,0 +1,46 @@
+package test
+
+import (
+ "encoding/base64"
+ "net/http"
+ "testing"
+)
+
+func TestApiKey(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+
+ login(s, u)
+ apiKey, summary := createApiKey(s, u, "test-"+s.id)
+ logout(s)
+
+ token := getSessionToken(s, u.username, apiKey)
+ deleteSessionToken(s, token)
+
+ login(s, u)
+ deleteApiKey(s, u, summary)
+ deleteUser(s, u)
+}
+
+func createApiKey(s *session, u *user, name string) (string, string) {
+ key := s.e.GET("/api/user/create/api/key/{id}").WithPath("id", u.uuid).WithQuery("name", name).Expect().Status(http.StatusOK).Body().Raw()
+
+ summary := key[:4] + "..." + key[len(key)-4:]
+ s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Path("$..Key").Array().Contains(summary)
+
+ return key, summary
+}
+
+func getSessionToken(s *session, username, apiKey string) string {
+ b := base64.StdEncoding.EncodeToString([]byte(username + ":" + apiKey))
+ return s.e.GET("/api/session/token").WithHeader("Authorization", "Basic "+b).Expect().Status(http.StatusOK).JSON().Object().Value("token").String().NotEmpty().Raw()
+}
+
+func deleteSessionToken(s *session, token string) {
+ s.e.DELETE("/api/session/token").WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).NoContent()
+}
+
+func deleteApiKey(s *session, u *user, summary string) {
+ s.e.DELETE("/api/user/create/api/key/{id}").WithPath("id", u.uuid).WithQuery("hiddenApiKey", summary).Expect().Status(http.StatusOK)
+ s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Path("$..Key").Array().NotContains(summary)
+}
diff --git a/ui/core/src/assets/fonts/.!86114!WorkSans-Medium.ttf b/test/assets/.!87400!test_template.odt
similarity index 100%
rename from ui/core/src/assets/fonts/.!86114!WorkSans-Medium.ttf
rename to test/assets/.!87400!test_template.odt
diff --git a/test/assets/.!87401!test_expected.pdf b/test/assets/.!87401!test_expected.pdf
new file mode 100644
index 000000000..543b76573
Binary files /dev/null and b/test/assets/.!87401!test_expected.pdf differ
diff --git a/test/assets/test_expected.pdf b/test/assets/test_expected.pdf
new file mode 100644
index 000000000..2ac94b36a
Binary files /dev/null and b/test/assets/test_expected.pdf differ
diff --git a/test/assets/test_template.odt b/test/assets/test_template.odt
new file mode 100644
index 000000000..cc232a64b
Binary files /dev/null and b/test/assets/test_template.odt differ
diff --git a/test/bindata.go b/test/bindata.go
new file mode 100644
index 000000000..bbe0896d7
--- /dev/null
+++ b/test/bindata.go
@@ -0,0 +1,262 @@
+// Code generated by go-bindata.
+// sources:
+// test/assets/test_expected.pdf
+// test/assets/test_template.odt
+// DO NOT EDIT!
+
+package test
+
+import (
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func bindataRead(data []byte, name string) ([]byte, error) {
+ gz, err := gzip.NewReader(bytes.NewBuffer(data))
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+
+ var buf bytes.Buffer
+ _, err = io.Copy(&buf, gz)
+ clErr := gz.Close()
+
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+ if clErr != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+type asset struct {
+ bytes []byte
+ info os.FileInfo
+}
+
+type bindataFileInfo struct {
+ name string
+ size int64
+ mode os.FileMode
+ modTime time.Time
+}
+
+func (fi bindataFileInfo) Name() string {
+ return fi.name
+}
+func (fi bindataFileInfo) Size() int64 {
+ return fi.size
+}
+func (fi bindataFileInfo) Mode() os.FileMode {
+ return fi.mode
+}
+func (fi bindataFileInfo) ModTime() time.Time {
+ return fi.modTime
+}
+func (fi bindataFileInfo) IsDir() bool {
+ return false
+}
+func (fi bindataFileInfo) Sys() interface{} {
+ return nil
+}
+
+var _testAssetsTest_expectedPdf = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x79\x09\x34\x94\x7d\xff\x77\x91\xa5\x11\xca\x96\xdd\xc8\x92\x7d\xf6\xc5\x5a\xf6\x08\xd9\xc9\x3e\x18\x8c\x65\x86\x31\x76\x12\xb2\x93\x10\x65\x8f\xc8\x16\xd9\x25\x6b\x54\xf6\x5d\x59\xb3\xdf\xb2\x86\xc8\x1a\xbd\x47\xdd\xef\xf3\x3c\xff\xbb\xe7\x7d\xff\xd7\x39\xd7\x75\xae\xef\xe7\xfb\xb9\x7e\xdf\xed\x33\x67\xce\xf9\xfd\x04\xb4\x95\x54\xc4\x21\x12\x70\x80\x40\x5b\x6e\x5b\x43\x5b\x4d\x5b\x16\x00\x0a\x04\x03\x09\xd6\x8e\x00\x19\x19\x90\x06\x16\x6f\x4f\x72\x00\xc2\x80\x60\xa0\x2e\x48\x05\xe7\x4c\xc2\x12\x41\x2a\xce\x18\x12\x56\x09\x6b\x43\xb0\xc5\xca\xc9\x01\xdc\x49\x44\x2c\xc6\x05\xe0\x9d\xee\xd2\x23\x4d\xd3\x7a\xf3\xf2\x85\xd9\xd3\x7b\xef\x3f\x29\xf4\x8d\xfb\xe1\x67\xd1\x60\x39\x40\x7d\x4b\x50\xe8\x18\xdb\xc6\x85\xe1\x83\x7d\x73\x6d\x0d\xbe\xe1\x5e\x30\x8f\xd8\x70\xb7\xcc\xc5\xd4\x31\xd5\xb5\x5b\x64\xa6\xd9\x3e\xd0\x7c\x66\xd9\x26\x1a\x87\xa6\xb4\xec\x94\xa7\xa4\x2e\xb5\x97\xf9\x9e\x79\xdf\x83\x22\x43\x27\x36\xcc\x5a\x0c\x72\x5b\xde\xf2\x01\x0d\xbf\x24\x0b\xf2\x2a\x0e\x38\x6d\xae\xe6\x71\xf5\x36\xa5\x6c\x83\xd6\x2a\x63\x6f\x20\xfd\xc7\x62\xb9\xf3\x3c\xcd\xc0\x42\xd5\x25\x19\xad\xf5\xa1\x6c\x2b\xb4\x96\x11\xc2\xa2\x00\x2c\xde\xf6\xef\x8c\xb0\x78\xdb\xb3\x22\x00\xb0\xbf\xab\x81\xc0\x10\xff\xc2\x10\x7f\x54\x88\xfc\x7f\x54\xf8\xb7\x1f\x02\x94\x44\xc3\xc1\xff\x59\xee\x5f\x68\x1f\x12\xc7\xf7\x94\x19\x14\xdf\xdd\x0a\xff\x5b\x44\x31\x1e\x9d\x60\x95\x94\x31\xad\x79\x9b\x74\x21\x2f\x85\x97\x1d\xc9\x71\x63\x6e\x9c\x3c\xf2\x44\x85\xf5\x48\x01\x53\x40\x4d\xae\x7a\x94\x22\x99\xf3\x54\xb2\x2b\xfa\x82\x02\x4e\x78\x63\xef\x59\xbd\x73\x9a\xaa\x4f\x27\xeb\x98\xe0\xd6\x5e\xbc\xeb\xe3\x80\x7a\xd3\xe9\x3d\xf1\xb2\x3c\x71\xdc\xb0\x84\x8b\x17\x8d\x58\x15\x05\xb7\xb8\xd2\xf4\xe6\xfe\x70\x87\x9b\xc2\xc4\xd7\xb5\xc9\x9f\x9e\xb9\x29\x87\x5f\x8f\xf7\x87\x1d\x86\x3a\xc9\x31\x42\x0f\x84\x2f\xbf\x95\x86\x66\x0b\x3e\xee\xaa\x60\x38\x77\xc0\x7d\x2e\x39\x46\xe3\x45\x5e\xff\xde\x54\x43\xca\x6c\xf4\xb9\x56\xa7\xe7\x91\xe5\x91\xa0\xac\xf1\x1d\xee\xfb\x73\x62\xf7\x57\x3a\xcb\xb7\x15\x9f\xd7\x37\x37\xad\x52\x9d\x0b\x5d\x38\x97\xfc\x3d\xbe\x8c\xfd\xa8\x3d\xf1\x95\x21\xf9\x63\x59\x05\xe4\x27\x95\x88\x81\x2c\x49\xdb\x6b\x3f\x3c\x5a\x9f\xa8\x08\x0e\xa1\x5e\xa1\xd6\xbf\x9d\x7b\xcc\x08\x58\x6b\x5f\xe0\x5b\x65\x98\x6c\xf3\x6c\x1d\x65\xbc\xf7\xf9\x81\xaf\xfb\xfc\xa3\x2d\xa7\xa0\xad\x73\x73\xba\x1a\x19\xd4\x33\xa3\x7e\xbb\x0c\x73\x28\x72\x26\xce\xdb\xef\xf4\x56\xbe\x60\x56\xc4\xee\x3b\x5b\xfd\x50\x82\xa6\xb3\x7e\x3e\xe8\x35\xa2\xb6\x04\x00\x3d\x2f\xd3\xae\xb4\x3d\x54\x20\xdb\x61\x95\x88\xa0\xb4\xc9\xec\x4c\x27\x08\x0b\x89\x05\xff\x0c\x0f\xdf\x46\xa3\x81\xf8\x06\xee\x97\x47\xdf\xa3\x14\xfd\x2f\x89\xcc\xb9\xdd\x1f\xaf\x65\xf5\x3c\x4c\x5d\x49\x3d\x69\xfb\x89\x65\xb2\xbb\xf7\x9e\x81\xd4\xe6\x35\xb7\xbf\x2c\x7f\x1e\xff\x9e\x02\x4c\xcd\x1b\xd1\x56\x02\x8e\xcd\x3c\x1c\x7a\x7d\xb1\x3e\xc2\x8b\xa3\x41\xeb\x07\x2b\xa7\x2a\xa3\xff\x86\x89\xe1\x60\xec\x47\x63\x25\x16\xca\xba\x29\x97\x2d\x86\x1f\x3f\x52\xbf\xde\xa0\xe8\x6f\xa9\x37\x3a\x02\x4e\x84\x6c\x51\xcb\x7c\x58\x1e\x53\xaf\x91\x8b\xeb\xb7\xb9\x1a\xd6\x41\x59\x39\xa5\xfc\x44\x76\x14\x2a\xb0\x9e\xdf\x1d\x40\x9a\xd6\x18\xbd\x78\xde\x63\x55\xba\xca\xda\x22\x44\xb6\x15\xd6\xca\xe1\x4a\xa6\xb5\xb2\xcd\xdc\x66\x3a\xa6\xb0\xaf\x15\xb5\x15\x83\x7f\xc6\x78\x24\x87\xd9\x15\x53\xc6\x04\x41\xd0\x17\xbc\x1e\x21\x6f\xc4\x8c\x06\xfe\xe0\xeb\xc8\x3a\xe1\x1a\x9f\xa0\xec\x67\xc8\x26\xc7\x07\x2d\xb8\xb3\xf3\x5c\xdb\xe7\x35\xb1\xf2\x7c\xe8\xa9\x48\x07\xae\x8e\x3e\x0e\xdd\xa4\x6e\xcd\xa3\xa2\xae\xcb\x5a\x9f\xa6\x17\x39\x7d\x94\x23\x23\x81\x6f\xb4\x0b\x38\x0d\x56\xe4\xbc\x7f\xcd\x4c\xc2\x50\xfe\xb4\xfa\x75\xab\x63\x99\xdd\x29\xf0\x65\x6a\x80\xaa\x19\x80\x85\xf5\x69\x00\x4a\x35\x6d\x23\xfd\x24\xa3\xe0\x74\xd3\x7f\xf1\x33\xe1\x29\xd0\xe5\xe9\x89\xf2\x52\xb2\x46\x47\x01\x2b\xcd\x60\x24\x62\x64\xdb\xb2\x2d\xae\xd1\xa5\x9e\x2a\x24\x28\xe8\xb4\x69\x3a\x43\x56\xfa\x4d\xad\x97\x07\x7e\xcd\x81\xe0\xf4\xbd\x16\x7e\x5c\xe3\x9b\x56\x63\xd6\xec\x33\xfc\x81\x76\x6d\xb4\xc4\xbc\xb2\x42\x07\x57\xf0\xca\xd4\x50\xcf\xbc\x5a\xdf\x62\xde\xd3\xe0\xf1\x84\xcb\xa7\xf9\x1c\xb2\xdb\x4d\xe3\x8f\xed\xe1\x11\x23\xcd\x52\x6a\x6d\x98\x66\x41\x43\xb5\x3d\x43\x83\xbd\x6e\x47\xbf\x80\x8c\xc5\x57\x11\x55\x23\xa3\x87\x7f\x7d\xc2\x68\xea\x0a\xc4\xe2\xc3\x1c\xeb\x1a\x33\x12\x6d\x0d\x12\xc3\x04\x4a\x52\xf1\xab\x32\xdf\xca\x96\x04\x3a\x07\x8b\xf9\x1b\xef\xaa\x6d\xa6\xf3\x4f\xe4\xf6\x96\xa5\xd4\x3d\x12\xd8\xc8\x63\xb9\x2b\x15\x1d\x22\x4d\x7e\x81\x87\x10\x1f\x92\xb0\x87\x0c\xaf\x75\x1f\xd3\xee\x8d\xbf\xa6\x30\x85\x1f\x9a\xd5\x29\x77\x10\x68\xfe\x24\xc5\x96\xb2\x47\xaf\x17\x83\xd6\xb3\xc2\x25\xa7\xb5\x25\x1f\x49\x3d\x2d\x67\xba\x6c\x7c\xf3\x51\x8d\x86\x56\xcf\x6c\x85\x78\xc7\x79\x4c\x4a\xe0\x42\xcc\xb9\xa5\x6b\x4b\xc9\xd0\xef\xc5\xbc\x25\xf6\xf6\x2d\x33\xf3\xa4\xfa\x90\xef\x1e\x5c\x26\xe5\x53\xad\x77\x07\x7a\x34\x1d\x7d\x7a\x53\x23\xd3\x1e\xe7\x88\x91\xd0\x82\xbd\x23\x19\x59\x4d\x5f\x5e\xd2\xee\xd1\x8e\xb2\x84\x2d\x3e\x7a\x73\xfd\x59\xa4\xc8\x8a\x56\xf4\xe9\xca\x8a\x34\xc2\xaf\xa4\xc6\x0c\x3f\x98\xcc\x73\xef\x79\x30\x94\xde\xc9\x83\x18\x6d\x59\x8c\x35\x94\xf6\xfb\xf2\x83\x64\xe1\x19\x99\x5a\xde\x25\x78\x7b\x78\x93\x3c\x8f\xaf\xf9\xe4\x53\x12\x93\x41\xfe\xa2\xc1\xf8\x34\x15\xa5\xa3\x42\x65\x58\xe3\x79\x94\x74\x7a\x89\xfe\xda\xca\x8a\xe1\x5d\xc7\x0c\xee\x4c\xc3\xfe\x3e\xad\x8d\x44\x81\x34\x19\x59\x3d\x63\x74\xc6\x96\x5e\x5d\xf6\xf5\x9b\x82\x7d\x7d\x72\x01\x3d\x85\xd5\xd3\x8c\x23\xce\x6d\x57\xa6\x58\x47\xf4\x08\x1b\x1b\x23\x2c\xeb\x53\x5c\x03\x0f\x8a\x0c\x85\x73\x18\x42\x9b\x34\x2b\xfc\x1a\x87\x5d\xb2\x12\x57\x0e\x6d\x9c\x9f\x55\x68\xbb\x15\xb4\xe2\x51\xa2\x6d\xe5\x95\x16\x63\xaa\x11\xc8\x6a\xd8\xa3\xf7\x02\x63\x5f\xfc\x39\x1b\xd4\x5d\xb4\x19\xc3\xd8\xbe\xf8\x3f\x5c\x60\xb5\x4a\x2f\xe4\xb4\xd7\x0a\x30\x7e\x31\xed\xb9\x47\xf1\x02\xa1\x68\xa0\x7a\x6b\x4f\xef\x7d\x5c\xe1\xc4\x88\x5c\x6a\xe9\xc9\x4b\x48\x85\xa6\xe8\x69\xcb\x4a\x72\xfe\xed\xf5\x21\x4a\x81\x0e\xac\x40\x54\x11\x1f\x53\x12\xdc\x06\xc1\x1c\x59\xb5\x24\x3a\x30\x05\x4e\xc2\x71\xa8\x46\xdd\xe8\x98\x9e\xf6\x4c\x08\x71\xf4\x8b\xac\x92\x17\x84\xf5\x3c\xb4\xdc\xa3\x5f\x9b\xe9\xbc\x45\xc1\xf6\xa8\x60\x60\xa3\x7a\x5c\x7e\x99\xde\x61\x9a\x85\xac\xa1\x68\x1a\xf2\x53\xe3\xb9\x02\xfb\x2a\x18\x21\x36\xd1\x9a\x56\x28\x4c\x54\x10\x4d\x0a\x70\xff\x8c\xc5\x1c\x3e\x0f\x38\x5f\x9a\x53\x8c\xcb\xeb\x65\xbf\x14\xb7\x6c\x92\x3a\xff\x65\x4d\xfd\xdb\x1d\xfa\x5a\x5b\x45\xab\x9a\xc5\x8c\xa0\xfc\x16\x98\x9f\x9b\x5d\xf5\xa2\x7f\x2b\x79\x31\xef\x58\xeb\x83\xee\x54\x5c\x57\xd1\xc9\x70\xd0\x40\xc1\x12\xbb\x67\x98\xc3\xe7\x58\xf5\xf3\xf9\xa6\x3b\x94\x85\x6d\xf2\x92\x97\xe9\x2a\xca\x9f\x03\xc4\x9b\x97\x17\xdb\xae\x2c\x56\x0e\xf1\x2c\xaf\x27\x4a\x7b\xd1\x51\x7e\x5a\x0b\xb2\x65\xeb\x49\xcf\xee\x0a\x4c\x62\x93\x9c\xed\x49\xcd\x3e\x3f\xd2\x63\x60\x27\x0c\x44\xbf\x07\x5e\x9c\x88\x00\x56\x33\x00\x8f\x91\xc3\x06\x30\x83\x94\x95\x2a\x9d\xb7\x92\x49\xb4\xa5\x84\x4a\xa6\x9b\xd7\x17\x73\x85\x2f\xb9\xd4\x2d\xd2\xd7\x2e\x13\x2f\x35\xb7\x16\xc4\xc7\xea\xb9\xfb\xf5\x99\x70\x2b\x76\x6a\xb9\xd7\x0f\x73\x91\xec\xe3\x8e\xa4\x82\xc6\x12\xbe\x3e\x85\xf7\xbc\x2c\xd8\x90\x64\x71\xd1\x7b\x07\x1e\xf6\x47\xd6\xba\x33\x14\x63\x29\x38\x7c\x85\x81\xe1\x7f\x3d\xe3\x8c\x87\x13\xee\xdc\xff\xeb\x43\x57\x5b\xf1\xcb\xab\x21\xa3\x66\x9b\x06\x3f\x99\xc7\x1a\xac\x75\x52\x5a\x34\xd7\x39\x5b\xa3\x0a\xab\x3f\x49\x7d\xb9\x6a\xe4\xb4\x70\xc0\x39\x71\x8f\xb9\x9e\xaf\x3d\xf9\x45\x2f\xd1\x7e\x06\x0e\x93\x58\xa3\x0c\xba\xb6\x39\x70\x3f\xb6\xcc\xab\xf1\x73\xe7\x3e\x59\xcd\x33\xec\x84\x9f\x00\x87\x38\xae\xe9\x59\xbe\x7d\x83\x45\xcd\x53\x3e\x8e\x94\x2b\x75\x7c\x70\xf1\x7d\x0a\xa4\x3b\x6e\xe2\x0e\xde\x9d\xdb\xae\x49\xfd\x19\xa3\xf4\x71\xc0\x77\x8f\xaa\xea\x43\x3b\xdb\x06\xa7\x2f\x31\x57\x37\x57\x39\x2e\xc9\x60\x9f\xdb\xb7\xd5\x1f\x91\xc7\xd1\x08\xc1\xe9\x8f\x83\x61\xb7\x23\xaf\x7a\xab\x93\x6d\x11\x8b\x5a\x0a\x62\x3a\x2e\x47\xf2\xf6\xe8\x82\x78\xed\x24\x36\xfd\xbb\x68\xc0\xef\xeb\x28\xb5\x66\x0f\x3a\xee\x5e\xd0\x82\x7d\x71\xa0\xe6\xb3\x0f\x15\xeb\x4d\xf6\xd0\xef\xe5\xf2\x91\x07\x3e\xe4\xed\xde\x5e\xca\xa5\x55\x82\xd9\xe3\xf4\xd2\x3f\xc4\xb1\x59\x09\xec\x98\xc4\x58\x54\xa2\x66\x4c\x85\x98\x2e\xf6\xb1\xd9\xe6\xb6\x47\x7e\x83\xcb\x9a\x2f\xf2\xc9\xe8\x44\x28\x2f\xa7\xf3\x96\x58\x68\xc9\x45\xeb\xc1\x0e\x2d\xec\x0d\xad\xa3\xf8\x1b\xc6\xa9\x37\xd9\x5f\xaf\xfb\x5a\x81\xb7\x94\x78\x4f\x14\xa4\x0f\x93\x38\x0f\xc7\xe4\x17\x74\xaf\x64\xdb\x0d\xf7\x47\x59\x02\xca\xe5\xeb\x10\xac\x29\xdc\x74\x15\xef\x29\xb1\x23\xec\x47\x39\x61\xdf\x58\x1c\x13\x1b\x42\xc9\x3a\x45\x47\xc4\x88\x63\xf9\xcc\x1d\x72\x2a\x04\xeb\xdd\x17\x62\xb2\x71\x83\x9a\xf0\x1c\xe3\xb0\x5c\x39\xe5\xc6\xcd\xc1\x47\x9a\x2d\x5f\xef\xa7\x25\xe7\x56\x6d\x65\xa5\x0f\xd7\xd4\xde\xcd\x0f\xae\x2a\xd4\x98\xbc\xa7\x73\x87\xf9\xb9\x20\xeb\xe8\xa0\xfb\x32\xd9\xd3\xad\x23\x00\x0d\x60\x5d\xb0\x72\x5c\x82\x3a\x8f\x2e\xed\x29\x88\x8e\x8a\xa6\xf7\x30\xc7\xfc\x1c\x46\x88\x5a\xc0\x82\x0a\x29\x35\xb0\x74\xd3\x75\xb6\x9e\x4e\x66\xb3\x85\x91\x2e\x9d\x68\xd5\x1a\x24\x4b\xc7\x5f\xba\xd8\xc2\xf1\x7d\x41\x8f\x74\x53\xaf\x84\x4b\x86\x59\x93\x9b\xbe\x7e\xf2\xb6\x6b\xc8\xe7\x85\xf2\x9d\xfe\x2e\x95\xfd\x54\x0f\x4f\x9f\x48\x66\x6c\xfd\xa4\xc4\xa8\xfe\x68\x4e\xe3\x54\x90\x93\xe5\xf7\x1f\xf3\x2a\x5f\xf0\x36\x87\xdc\xf6\x33\xc7\xa2\x96\x1b\xae\xec\xf3\x79\xcd\xed\x21\x29\xf5\x81\x96\xdf\x06\xc9\xb5\x8e\xa4\x79\x6f\xae\xad\x4f\x5a\xdf\xf4\xcf\xcc\xcc\xbe\x1a\xb2\xfa\xd9\x18\x07\x97\x30\x36\x76\x30\xb6\x7a\xdd\x57\x3d\x50\xc6\xf7\xda\x95\x77\xcc\x39\x39\x43\xde\x94\xed\xf1\x06\x75\xf0\x06\xd7\xd5\xd3\x62\x9a\xfe\xba\xad\x2a\x9a\xb8\xd7\xe0\xaf\xad\xbc\x57\x91\x75\xda\x6e\xbd\xca\xbe\x7c\xa1\x17\x99\x99\x1c\x2f\x3a\x17\x68\x3b\xde\x7d\x5a\x87\x36\x42\xce\x17\x55\xbc\x5f\xf6\x7c\x6c\x11\x9e\x69\xb0\xc6\xf9\x5e\x08\x54\x68\x51\xe9\xeb\x72\x98\x20\x13\x6e\xef\x41\x3e\x1f\x98\x75\xec\x69\x27\xbf\x70\x30\xdd\xc2\xb5\xa6\xb5\x7f\xb2\xda\xda\xfd\xb3\xc1\x67\x38\x4e\xdd\xf1\xfd\x97\xe6\xfd\x10\x8b\xc9\xab\xa7\x5d\xb5\xde\xf5\xf5\x7b\xa8\x27\x72\x07\x2e\x07\xb0\x40\xaf\xf6\x77\x8e\x7b\xe7\x42\xcb\xb0\x7b\xe2\x73\x9a\xdb\xf5\x65\x97\x74\xa7\x0d\x2b\x3b\x17\x8d\x17\x49\x26\xf2\x9e\xc6\x5c\x12\xe1\xf3\x97\xcf\x3b\x8f\x30\x94\x0b\xd1\x73\x8a\x26\x77\x7f\x02\x94\x2b\xac\xd2\x49\x14\x95\x45\x86\x7e\x96\xaf\x34\xb6\x33\x0f\xca\x37\x9a\x8d\x67\x66\xb5\x0c\x52\x14\x75\x6d\x07\xe8\x1b\x2e\xa7\x68\xd7\xc9\x8a\x5d\xa1\xef\xb7\x25\xef\xb7\x71\xd8\x8c\x45\x55\x28\x2d\x15\x3d\xcc\xf3\x79\xac\x10\xfb\x59\xaf\x0f\x6a\x52\x6f\xe7\xe1\xa1\x49\xae\xab\x92\x69\xf0\x17\x5d\xca\xcd\x14\xa8\xa8\x38\xdf\x8b\xad\xa5\x4a\xfb\xfa\x3e\xa1\x26\x47\xc9\xba\x95\xb0\xa0\xe9\x95\x63\x7a\xd9\x9d\xd9\x31\xe1\xac\x2a\x74\x51\xd5\xb1\xd7\xe7\xed\xba\xb6\xd7\xf3\xc5\x33\xae\x4f\x80\x53\x12\xa2\x19\x1c\xa3\x87\x8e\x6b\xcd\x09\xe9\xbe\x0e\xdb\x04\xeb\xe8\x62\xa4\xcf\xa7\x2a\xb1\xe2\xc0\xfb\x44\x0f\xbd\xd2\x19\xb0\x3f\x7b\x97\x55\x88\x57\x7c\xde\xeb\x31\xfe\x28\x9a\x2e\xd1\x4b\xca\xc9\x39\x5c\x6f\xfd\x5f\x75\x5a\x8b\x53\x94\x6d\x5e\x62\xb8\x81\x12\xa6\xb1\xec\x8a\x55\xaf\xa1\x12\xe5\x88\xd5\x77\xab\x6f\x78\x71\x51\x96\x2e\x4e\xb1\xe6\x49\x32\x96\x23\x68\xa6\x6e\x27\xc8\xf4\xeb\x8d\xad\xd9\x68\x50\x69\x89\x69\xe5\xc9\x9b\xe3\xdb\xf4\xf9\x0f\xd2\x6a\x3e\x35\x1c\x39\x2e\x0e\xd5\xd6\x1c\x39\x93\x3f\x14\x58\x0c\xe4\x92\x2d\x9f\x56\xab\x2e\x6d\xdf\xf4\xb6\x8f\x65\x66\x8e\xbe\x0a\xaa\x35\x11\x2f\x23\xad\x10\xbb\x89\x79\xd9\x78\x65\xaf\xb2\xbe\x37\x24\xb7\xd0\x8f\x15\x0d\x76\x3e\x5e\xae\x18\x9d\xd8\xb6\xaa\x67\x28\x07\xf8\xb8\x92\x17\xe1\xb2\xd2\x64\x6e\xf0\x31\xd3\xba\x51\x99\xc1\xdb\x6a\x80\xa1\xa8\xf1\xb3\x59\x3f\x40\x53\xf6\x6c\xb3\xe8\x41\x4e\xf0\x8c\xfe\xd6\x6d\xc0\x74\xb6\x95\xc9\x5b\x6d\xad\x0a\x5d\xf9\x90\xe0\xf8\x99\xbe\xc4\x63\x99\xab\xec\x06\xae\xe4\xb5\x09\x4f\x6c\x64\x3b\x35\xa7\x4a\x67\xf9\x47\x67\xf2\x66\x1d\x29\x9e\x88\x18\xbf\x98\x05\x31\xfb\x4b\x5c\x77\xad\x0d\x0e\x67\x8a\x21\x06\xb1\x70\x03\x0e\x3b\x8c\x5c\x65\xef\x1e\xa2\x28\xb6\x65\xae\xfa\x2b\xbd\xb5\x01\xd0\xed\x80\x4d\x5e\xa7\xd4\x39\x34\xbf\x7a\x66\xe4\x23\x2f\xaf\xf9\x96\x20\xfa\x74\xb6\xcf\xc4\xfb\xe9\x2b\x21\x0d\x53\xe0\x07\x26\xfb\x5d\xa1\xb1\x5c\xa0\x90\xc3\x74\x1a\x57\x71\xf6\xba\x49\x1f\x77\xe7\x14\xe4\xb1\xc6\xd7\x46\xb7\xe9\xed\x42\xa7\x0e\x33\xe1\x78\xc6\x94\x34\xdd\x0e\x97\x4b\xeb\xb3\x6b\x71\xa8\x8c\x40\x75\xe4\xf2\xc2\x9b\x87\x9a\x82\x19\x49\xc6\x5f\xad\xa4\x3f\xac\x3a\xfa\x44\x16\xf6\x60\xb2\xc2\x57\x5c\x7b\xd9\x51\x72\xd3\xc4\x3b\x3f\x16\x8f\x9e\x3c\xb4\xdf\xb3\x5b\x3d\xae\x3d\xaa\xd9\xa8\xb0\x3d\x3f\xd3\x52\xaa\x71\x92\x29\x8b\xe6\xa5\xeb\xda\x08\xa7\xb7\x9f\x6e\xd8\x75\x08\xed\x37\xe2\x15\x3a\xf1\x33\x7a\x69\xec\xc5\x56\x58\x37\xd2\xf5\x96\x5e\xd4\x7f\x1d\xc4\x28\xd6\x43\x62\xe3\x4f\xcd\x82\xba\xbe\x7d\xec\xd5\x7e\x4d\x48\x44\x44\x18\x95\xcc\x2a\x50\x6b\x23\xef\x35\x80\x11\x17\xae\x60\xf1\xa0\x2a\xd1\xda\xf1\x90\x95\x8f\xae\xd3\x28\x58\xbf\x18\x6c\x45\x8e\x52\xc7\x57\xd5\xe1\x4e\xdd\x86\xf7\x3f\x8f\x2d\x7c\xd8\xc1\x8f\x4c\x1d\x94\x71\x30\x72\x7e\x7f\x53\x3e\x98\x31\x79\xbf\xc2\xe3\x4d\x8e\xc4\xf8\x7b\x70\xf3\xb5\x97\x2c\xfa\x61\x50\xd5\x42\x20\xa8\x30\x5b\x76\x85\x38\x84\x34\x24\x37\x61\x3e\xc8\x4b\x6b\xda\x6d\xad\xcb\xf2\x0f\xfc\xe9\x3d\xbd\x9b\xe7\xa5\xd2\xbe\x85\xbc\x94\xf1\xa6\x3c\x2d\x13\xf0\xde\x54\xfa\xde\xe1\x1c\x55\x4a\x5c\x7a\xd3\xf4\x13\xe3\x7b\x61\xed\x22\xfe\x29\x1f\xd1\x77\x03\x70\x89\x21\x9b\x6a\xdd\x42\xe3\xd0\x05\xb7\xa3\x2f\xe8\x55\x17\xca\xe7\x83\xba\x56\x7d\xcf\x37\xc2\xee\xb1\x29\xd9\x78\x69\x93\x1c\x28\x0e\x5c\xcf\x67\x0a\xb5\x14\x92\xad\x0b\xdf\x24\x9c\xb7\x13\x6e\xa3\x58\x16\x69\x89\xa6\xf0\xce\xd4\xfb\x2a\xda\xf2\x82\x4c\x1f\xb0\x45\x6c\xab\x0c\x39\xb7\x03\xb3\x0e\x28\x79\xc3\x79\xba\x52\xae\xd1\xb9\x0c\xbe\xe7\xfb\x48\x75\x4f\xb1\x48\x46\x56\xa6\xa7\x49\xe0\x54\xc3\x9f\x25\x5d\x9b\xc2\xcb\xcf\xaf\x4b\x54\x04\xb0\xf1\xa2\x48\x44\x47\x84\x89\xc6\xa9\xa8\xbb\xfb\x2d\xde\x72\x84\xab\x6a\xf2\xad\x75\xbc\x17\xa5\x66\x72\x85\x4c\xb9\xf4\xd3\x2f\xf7\x87\xf0\x13\xdf\xb2\x29\xa5\xfd\x1c\xb7\xac\x69\xb5\x3e\xce\xba\xd2\x32\xac\x97\xc8\x77\x64\x5f\xe8\xa7\xe5\x33\xb0\x98\xf9\xf1\x65\x4f\xf3\x94\xa6\x05\xbf\x13\x6d\xd9\x1a\xa2\x12\x79\x07\x43\x7b\x02\xb8\xb5\xf7\xd1\xb4\x2c\x12\xa9\x75\x4a\x53\x63\x47\x23\x01\x66\x7e\x1e\xb2\xa9\xd9\xca\xa0\xbf\x6f\xd7\x74\xf7\xd3\x89\xd1\x74\x4f\xa8\x79\xa0\xfb\xea\x56\xf9\xe9\xad\xea\xcd\xe8\x7d\xad\xfd\xe2\x8c\x34\x0e\x53\xdf\x77\xf6\x30\x1e\xcb\x23\xc7\xd0\x57\x01\x9a\xb4\x86\xeb\x37\x88\x8d\xf2\x0d\xc7\x83\x1f\xa9\x4e\x9f\x37\x80\xf5\x24\xd8\xf7\x54\xa8\x7c\x17\xd7\x37\x2f\xf4\x34\x61\xad\x08\x21\x51\x73\x0d\xfb\x95\x82\xbc\xcd\xc6\x42\x35\x13\x51\x9a\x87\xd7\x64\xc9\x3c\xa8\x68\x2e\x19\xc2\xac\xc6\x2d\x27\xb7\x58\x38\xbe\xd6\x60\xa2\x69\x9f\x4c\x0c\x58\xb5\xa1\x9c\x0f\x82\x69\x79\xca\xe7\x36\x90\x97\x7a\x9f\x13\x7c\x43\xf4\x22\xf5\xbb\x42\x2d\xe6\x88\x77\x96\x22\x4e\xcc\x76\x65\xfb\x8e\xdc\xb4\x66\x9c\x14\xb3\x07\xc3\x12\x68\xdf\x86\x8d\x97\x58\x9d\x9a\x34\x5b\x47\xf6\x8b\xe3\xe9\x45\xf9\x1f\xfb\x2d\xad\xaa\x9a\xbe\x8e\x19\xc1\x85\x77\xa6\x34\x5e\x14\x0f\x87\x52\x05\x1c\x54\xbe\x23\xdc\x63\x55\xbc\x73\xbb\x10\xad\x5e\x62\xe9\xbd\x72\x23\x34\x94\x04\xc2\xd6\xd5\xdb\x05\x62\xc7\x34\xea\x9e\xb4\x8d\x36\xd8\xb0\xf0\x19\x97\xe4\x7d\x18\x8a\xea\x22\xae\x12\xea\xda\x92\x9f\xdc\x69\xbb\x9b\x40\x60\x8e\x1e\x13\x34\xaf\xa9\x62\xf0\x32\xb2\xc6\x09\xb0\x8b\xb8\xb5\x89\x26\x1d\x28\x08\x4c\x5b\x44\xae\xa8\xaf\x17\x2a\xbd\x35\x8d\x8c\x4d\x72\x28\x7e\xe7\x25\xba\x6d\x2d\xe0\x98\x84\xee\x61\x2b\x55\x97\xe9\x1f\x0c\x17\x85\xe8\x44\xaa\x0b\xf4\x75\x5f\xab\x6f\x16\x78\x65\xf1\x34\xcb\xf8\x31\xbc\xfd\x85\x4c\xca\x8b\xe0\xa1\xae\x53\x70\x79\xa4\xdd\x71\xfc\x96\x73\xb7\x6c\x64\x4f\xbb\xdf\xf6\xad\xb4\x57\xc5\x4e\x57\xb9\xa7\x12\x7d\x9e\x19\xe1\x55\xc3\xcb\x8a\x5b\xbd\x56\xac\x7e\x4e\x0b\xb4\xd6\x01\x66\xda\xf3\x0f\xe4\x42\x76\xcb\xfc\x34\x58\x19\x90\xf7\xa9\xe9\x08\x72\x11\x94\x7c\xad\xc3\xf2\x13\x0f\x2a\xfb\x0c\x91\x12\xcf\x2a\x3c\x00\x79\xde\xeb\x23\xc2\x4d\x20\x8f\xe1\x23\xef\xa9\x31\x1a\x96\x89\x94\x64\x9f\xdb\x1b\x1f\xc8\xc0\x0a\x3e\xa1\x75\x68\x66\x78\xda\x83\x21\x7d\xed\x82\x74\xbf\x82\x94\x35\xb2\x8d\x5b\xb7\x2f\xd4\x97\xbc\xab\x2c\xba\xe0\x52\x15\xbd\x5c\x76\xe0\xfd\xe2\x24\x69\xd4\xc2\xef\x24\x69\x14\x76\x03\xbf\x31\xf5\x69\x71\x63\xec\x93\xa0\x0c\xb1\x90\x4b\x71\x2c\x6e\x52\xa7\xf7\x87\xcb\x12\xbc\x54\xd5\x6f\xa9\xad\xa7\xae\x67\x38\x65\x26\x47\x31\xeb\x4e\x14\xb9\x1d\x41\x33\xc0\xaf\xe3\xcd\x61\xc0\xdb\xa4\x80\x2c\xef\xbe\xc2\x26\xbf\x43\x17\x9d\x17\xa2\x9b\x5a\x13\x06\xda\x81\xe3\x05\x6b\x29\x53\x6c\x3f\xff\x42\xd9\x1c\x4d\xce\x75\xab\xba\x4d\x53\x4e\x78\x79\x06\xf1\x73\x87\x1b\xba\x41\x9c\xaf\x70\x53\xd4\xb6\x4c\xcb\xca\x02\xd6\x2e\x09\x1b\xdf\xf2\x30\xb1\x3b\xb7\x55\x7e\xb5\xef\x41\x7d\xc4\x8c\x9a\x2b\x6a\x94\xae\xe5\xae\x73\x0b\xeb\x87\x07\x7e\xca\xf2\x53\x82\xf7\xfa\xcb\x7a\xb7\xd2\xe7\x54\x3f\x28\xbf\xc9\xb7\x76\xb0\x72\xdb\x59\x76\x79\x56\x86\x8a\x6a\xbd\x57\x94\xdc\x51\x37\x8a\x71\x96\xc8\x34\x77\x2f\x1f\xdc\x65\x0c\x7b\x93\x7c\x5e\xc5\xc3\x38\xf7\x04\xd7\x4c\xc7\xb1\xec\xb3\x5b\x40\xc7\xd1\xd9\xfd\xc4\x03\x4d\xaf\x3e\x97\x4b\x97\x36\x0b\xfe\x11\x42\x97\x96\xcd\x34\xba\xea\x2a\xc7\xb4\x80\xe4\xaa\xd1\x5e\x3f\xcf\x25\x71\x5f\xb9\x35\xda\x9d\xd0\xcf\xf1\x20\x6b\xcd\xf5\xca\xcb\x2b\x2e\x52\x66\x97\x68\x94\x6f\x44\x4f\xb7\x4c\x1f\xc9\x0a\xd2\xad\x80\x65\x47\x39\x00\x24\xe3\x73\x95\xd9\x86\xe1\xca\xc6\x4e\x5b\xcb\x65\xe7\x0d\x59\x73\xa4\x9c\x1f\x28\x87\x51\x91\xd0\x71\xab\x7e\x26\xe1\x9a\x5b\x99\x73\x54\x52\xce\x61\xd8\x0f\x81\x83\x73\xfe\xc6\xb6\x3f\x32\xe8\x4c\x1f\x8c\xee\xbc\x94\x32\x3b\xb7\xd6\x4b\x58\x75\xe5\x5d\xef\x7c\xe2\xf1\x9a\x7a\xef\x0b\xcf\xe0\x9c\xb5\x71\xc6\x4f\x4b\xf1\xc1\x72\x07\x29\x5f\x34\x67\x82\x82\xa3\x49\x92\xca\x7c\xa4\x62\xe7\xb2\x31\xbf\xb8\x38\xc7\x5d\x89\x12\x47\xb3\x71\xa1\xea\xf7\x0b\x36\x0b\x98\x42\xc9\x4c\x8d\x77\x89\x8e\x52\xec\x71\x08\xe3\xd4\x57\x2c\xa6\x02\xdf\x13\x22\x3d\x11\xe6\xd7\x3d\xde\xa9\xb0\xbf\xc9\x39\x6e\xbd\x7b\xb7\x10\x94\x1e\xf3\xf0\x68\x7e\x21\xbb\xa4\x30\xde\x55\x8c\x49\x4b\xfd\xfb\xd0\xc7\xf5\x62\xb7\xc8\xce\x4d\xc0\x75\xbd\x98\x8a\xa2\xec\xf2\x72\xcc\x47\xec\x15\x09\x9d\x7e\xfe\xa5\x42\x65\xbd\x04\x91\xbc\x44\x47\x21\x99\x6c\x41\x42\xc2\xfc\x73\xa3\x0f\x7c\x02\x6d\x36\x83\xfa\x52\xe9\xfc\xd6\x70\x41\x5a\xf8\x2e\xe2\x96\xac\xa3\x14\xe7\xed\xdb\x8e\x62\xfc\xdf\xfb\x85\xbf\xb2\x72\xc5\x3e\x7c\x20\xe9\xc6\x9f\xae\x51\xcc\xcf\x79\x47\x55\x68\xf9\x84\x2a\x61\x50\x5f\x48\xaa\xb5\x21\x8f\x7d\x89\x98\x62\x94\xd4\x44\xf2\x86\x8a\xb9\x18\x0d\xaf\xb8\x8a\x70\xef\x14\x0a\x7f\x53\x8e\xff\xae\x6a\xb3\xad\x9f\x41\x02\x33\x70\x45\xde\xa2\xfa\xf8\x98\x09\x90\x9f\x63\x03\xb1\xfe\x2a\xcd\x6f\x5e\xc8\x77\xc5\x28\xcf\xcc\xba\xfc\x80\x3f\x25\xfd\xd2\xa8\xce\xd4\xf3\x4e\x57\x11\x86\xd8\x58\x35\x61\x8b\x94\x97\x35\x8f\x1f\x16\xe0\x92\x77\x95\xd0\x19\x31\x0f\x0b\x16\x16\xc2\x0b\x3c\x18\x2f\x0e\x4d\xa8\x36\x4a\xb1\x6a\xaa\x76\x6c\x2b\xa1\x05\x3c\xc2\xe9\xd5\x1b\xb7\x2f\x22\xf6\x1c\x14\x91\x4c\x45\xbd\x42\x06\x86\x51\x5a\xfc\x09\xdb\x6a\xdc\x36\x01\x8e\xfc\x6c\x74\xed\x1d\x29\xec\xba\xfb\xca\xed\xcf\x71\x15\x2c\x3a\x89\xab\xd4\x06\x31\x5d\x2c\x5c\x6a\xdb\x51\xda\xbc\xb9\x0f\xd8\x8b\xf7\x79\x6e\x7d\x60\xc4\x44\xed\x86\xd5\x94\x76\x07\x33\x59\xd5\xa4\x16\x1e\x1c\xf0\xfb\xd8\x0d\x95\xbb\xa2\xdf\xc9\xe8\x8e\x25\xce\xb9\xe9\x5e\x6b\x5f\xd8\x3e\xd0\x14\xcb\x1a\xf4\x4e\x20\xa3\xcd\xe3\xfb\xbe\x73\x8d\x27\xb6\x7d\x28\x97\xfd\xc9\x88\x76\x04\x43\xbe\xe3\x7b\x55\x3a\x8d\x32\x39\x9d\x94\xe1\xba\xd9\x91\x78\xdb\x74\xef\x48\x37\x84\xa6\xa1\xa0\x2e\x6d\x3c\x49\xaa\x5c\x47\x87\x8e\x5b\x66\xec\xd9\x6a\x09\x5b\x76\x1f\xfc\x11\xf6\xaf\x3b\xdb\x43\xe1\x0c\x98\x46\x1e\x35\x3b\xac\x75\x43\xf4\x04\x8f\x4d\x89\x74\xcb\x71\x77\xc2\xf2\xda\x72\x82\x27\x7f\x81\x5c\x09\x53\x7a\x2e\xcd\x88\x5a\xdd\xa1\xd9\x78\x99\xae\x97\x3c\x36\x46\x70\xa8\x30\x4c\x9f\xd4\xc0\x9b\xc9\xa9\xe7\x59\xe5\x29\x4f\xa3\x84\x89\xe9\x83\x67\x84\xf3\x5e\xe2\x3d\x32\x62\x23\x4b\x72\x4d\x7a\x76\x25\x42\x91\x8c\x4e\x3e\xe1\xb6\x72\x84\xc8\xa0\xe2\x2c\xb5\x58\xfe\xb1\x88\xc3\x83\x24\x92\xfa\x80\x8a\xae\xdb\x7c\x5c\x62\xd3\x85\xde\x0b\xd7\xd5\xa2\xcd\x0d\xf0\x28\x3a\xc8\x84\x96\x78\xfa\x4e\x7e\x49\xc9\xb5\x02\xa3\xb8\x9c\x0c\xa7\xd1\x2b\x23\xf2\x47\x7c\xb6\xd9\xd1\xbb\x7b\x57\xbc\x9a\xcc\x2f\x1e\xa1\x05\xd4\x27\x4b\xe2\x84\x16\x72\xeb\x99\x75\xf9\x79\x04\xf1\x11\x3b\xfc\x05\xf9\x3b\xfd\xb8\x9f\x92\x4d\x70\xfb\xc0\xab\xf9\x79\x4b\xb6\x86\x3a\xce\xb1\xbb\x38\xe0\xb2\x89\xa1\xc4\x64\x41\x56\xa5\x9b\x0b\x41\xa7\x42\x64\xbb\x2b\x5f\x32\xfc\xfd\x0d\xf9\x7d\xfe\x29\xa5\x2a\x74\xdc\x7c\xdc\xa8\xea\xb5\x3a\xf0\xa6\x5d\x14\x1a\xf3\x30\xf4\xb5\x81\x9d\xdf\x7d\xfe\xa3\xd7\xfa\xf4\xa3\xb6\xdf\xd5\xdc\x09\x10\x89\x8f\x81\xb4\x71\xd2\x1d\x0b\x42\x2b\x71\x5b\x7d\x4b\xfc\xaa\xaf\x5d\x90\xbc\x77\xbb\xdd\x82\xf9\x7c\xf3\xf6\xdb\xce\x67\x3a\x45\xf1\xa7\x58\x48\xd4\x32\xf9\x9c\xda\x5a\x96\xca\x8a\x06\xc8\xbc\x51\xfb\xea\x2e\x9d\x0f\x93\x79\xef\x23\xd7\xaa\xe7\x6b\x31\x9e\xfa\x7d\x47\x68\x5e\x09\x5b\x8d\xbe\x15\x0f\x6e\xbe\x42\x57\xdc\x90\xc9\x5b\xda\x6b\x27\xf1\xc2\xe9\xeb\x73\xe1\xc7\xa7\xb9\x3c\x6a\xa3\x06\xdf\x9b\xde\xbd\x8e\x53\x93\x55\x7f\xa1\x97\x3f\xea\xf7\xd8\x52\x6f\x8a\x10\xe3\x7f\x4b\xa6\x69\xaa\xa7\x67\x5f\x7c\x5b\xaa\xb0\x61\x75\x5c\xeb\xce\x65\x54\x2f\x56\x35\x42\x4e\x2b\x4b\xf1\xa8\x28\xed\x59\x91\x6d\xc0\xc6\xf0\xe8\x47\xf9\x26\x75\x3e\xdf\x0f\x8c\x53\xa5\x4a\x71\x30\xcc\x7c\x59\x61\xc4\x4e\xd3\xfb\xa9\xdb\xde\x71\x20\x23\x01\x69\xd4\xf3\xa3\x1e\x75\x33\x94\xbc\x6b\xba\x7d\x7c\xb1\x8d\xec\x26\x6d\x37\x7f\x0f\x1f\xf6\x51\x97\x57\x70\x40\x2e\xa4\x2f\xc0\xde\x26\x7a\x6b\x45\xdb\x87\xd0\xe6\xfb\x6a\x58\x26\xc4\xac\x51\xc1\xf7\x85\xd8\xea\x85\xfc\x91\x90\xac\x7e\x0a\xd3\x9f\x98\xc6\x17\xa8\x81\xa6\x41\x0f\xcd\xe8\x71\x40\x5f\x56\x2f\x93\x25\x22\xd1\xb7\x4f\x72\x12\xc2\xed\xe2\x20\xd1\xee\x5c\x9d\x32\xf2\xfa\xab\x70\xda\x36\x7a\x26\x0b\x9f\xba\xd6\x2f\x94\x55\xeb\xac\xac\x38\x65\x72\xc2\x53\x78\x28\xf4\x4a\xd3\x64\x9f\xfd\xcb\x95\xd0\xe5\xc4\x0d\x6f\xcf\x9a\xf9\x5c\xbf\xfb\xfb\x7c\xf5\x99\x15\xc3\xe9\xa7\x4a\x7f\xe9\x5b\xcc\xbc\x2e\x7d\x02\xa4\xdb\x49\x8a\xc5\x06\xbf\x8a\xa3\xce\x6a\xb9\xe0\x9c\x4b\xa5\x15\x9a\x63\x93\x45\x29\x17\xbe\x63\xd4\x1b\xd1\x64\xc7\xbe\xfb\x34\xa9\xd9\x38\xee\x7d\xd2\x2c\x73\x7a\xb3\x5c\x82\x7b\xb9\x86\x76\x8c\xf9\xd1\x6a\x5c\x6f\xc9\x1b\xf2\x9d\x4f\x84\x52\xc6\x80\x2a\x2a\x8f\xed\x93\x6b\xd9\xfd\x77\x8a\x2d\x24\x57\x44\x68\x9c\x3f\x9a\x35\x06\x96\xf2\x07\x8a\xbe\xea\xba\x26\x1d\xab\xd3\x3a\x2c\x93\xc9\xf4\x0a\x4b\xde\x8f\x89\x39\x90\xee\x50\xbc\xea\x57\xb3\xd4\xfe\x92\x23\x33\xdc\x89\xd3\x94\x6f\xb6\x15\xcc\x99\xbd\x03\xcf\xb8\xa6\xe2\x7a\x89\x85\xc1\x85\xf5\x8d\x1b\x35\xa5\xab\xa8\x59\x9c\xc5\x1b\xbf\xf3\x83\xaa\xfb\x76\xad\xe3\x8d\xe7\x5f\x85\x0b\x27\xd4\x67\x2e\x9b\x52\xcf\x68\xec\x6a\x56\x85\x9a\xac\x14\x0d\x8c\x2b\x7d\xb9\x22\x45\xfb\xe8\xce\xe8\x4b\x0d\xbe\x4e\x21\x92\x11\xa9\x21\x73\x4b\x6a\x48\x52\xd3\x68\xf1\xfa\x73\xe4\x95\xa4\xcf\x2b\x6a\xdc\x93\x2b\xd3\x2f\x66\x4c\x23\xbf\xe6\xd7\x5b\xb6\x4b\x53\xe7\x4b\x7b\x0f\xbe\x3b\x5d\xf9\xb6\x36\x3a\x38\xeb\xd2\xbe\xf3\x64\x6b\x57\xe0\xc1\x6d\x07\x1d\x39\x85\x03\xea\xe8\x7b\xf7\xdc\x62\xd7\xf2\x33\x69\xc8\x6f\x6c\x37\xc8\x9d\xbb\x2a\xf4\x89\x26\xf6\xaa\xaf\x84\x86\xf9\x80\x04\x2b\xe8\x19\x7a\xeb\x8d\xf1\x52\x54\x73\x8c\x22\xa8\x79\xa7\xa2\x6c\x01\xeb\xec\x4b\x67\xde\x7f\xb2\x2b\x39\xb7\xbe\xee\xb3\x69\xd4\x67\x1a\x65\xe2\xd7\x47\x9b\x75\x3b\x9d\x31\x0b\x67\x3a\x97\x13\xa8\xdc\x95\x92\x0d\xdb\xcf\x90\x0c\x58\xca\xb7\x8f\xd9\x6e\x56\x5d\x4b\x2c\xbd\x1e\x7a\x90\xb8\xec\x6b\xdd\xce\x58\x79\x27\x22\x9f\x7b\xa7\xee\xe5\xf5\xb8\x74\x26\xad\xac\x77\xef\xd4\x9c\xa5\x22\x6b\xc2\x73\xc3\xd9\x79\x18\x18\xe9\xe7\xae\xf2\x98\x2d\xea\x36\x0d\xec\x52\xbc\x3d\x94\x6c\xf8\xe9\xd0\xe1\x71\x64\xd1\x60\x1f\xde\x4d\xa1\x1a\x1e\x07\xb8\xfe\x30\x67\x4d\x28\xf3\x40\xec\x5e\x98\x50\xbc\x5a\x15\xd7\x23\x3a\xea\x95\xb9\xbb\x46\x8e\x0e\x9f\x92\x80\xed\x6e\x52\x36\x2e\x95\xa5\xea\x77\xde\xdf\xef\xe4\xa3\x5b\x8c\xac\x94\x60\x99\xad\x55\x61\x97\x29\xf9\xb2\x3f\xa7\xcc\x1c\x1e\x6e\x58\x1f\xf6\xf4\x6d\xe3\xf2\xe7\x34\xcb\x11\x5f\xdb\xfe\x00\xbf\xa6\x26\xd5\x29\x59\xe3\x35\x5d\xca\x2b\x94\xe1\x54\xa1\x8c\x21\x9b\x74\x80\x52\x79\xb3\x59\x61\x36\xb0\xee\x12\x65\x85\x2d\xb3\xb9\x75\xe2\x6b\xc5\x98\xe8\xfd\xc8\x2f\x4a\x89\x75\x84\xb4\xc9\x8f\x8a\x76\xf3\x9f\x55\xdf\x6c\xae\xb1\xd4\xd6\x76\x96\xba\xb8\x24\x74\x4f\x3c\x9c\xde\x23\x73\x72\x12\xa5\xb0\x76\x71\xaf\xbe\x18\x3d\x91\x41\x8e\xe5\x6e\xbf\xcf\x55\x42\x8e\x8c\xf3\xb8\x79\xd3\x31\x8a\x8a\x92\x69\xed\xa5\x41\x99\x49\xf8\x4b\xd5\x97\x39\xcc\xa9\x37\xd1\x3a\x61\x2a\x32\xd1\xd7\xed\x8e\x8e\x1d\x9d\xa4\xfb\x53\xed\xfc\xa4\x11\x7a\xfd\x78\xda\xcd\xa1\xa2\xa3\xb1\x8f\xf5\xee\xda\xdd\xf5\xb7\x43\x82\xde\x7e\x0e\x62\x7b\xe7\x17\xa2\x1a\xc7\x43\x35\x33\xaf\xe5\x62\x60\x79\x93\xc3\x82\x73\x7d\x44\xe1\x82\x37\x1b\x58\xf9\xbe\xcb\xb6\x06\xdb\x41\x70\xa1\xfe\x9b\x64\x9e\xa7\x45\x25\x9a\xa3\x64\x19\xd5\x7e\x7a\x9c\x77\xf2\xe8\x2f\x27\x87\x4d\x4c\xe8\x47\xdd\x1e\x50\xbb\x25\x15\xaa\x74\xeb\x8a\xac\xb2\x57\x30\xee\x2f\xb0\xa8\xb0\x46\x1e\x6e\x58\xcd\x46\x3c\x77\x2c\x45\xf0\xc7\x39\x3d\xbb\xa5\x88\x32\x94\xf2\x65\xe1\x81\x8b\x77\x9f\x54\x4f\x89\xed\x27\x2b\x8b\x3a\x88\x9f\xd0\x8b\x5e\xf0\xaf\xfd\x2f\xbb\xe0\xc8\xbf\x77\xbc\x11\x28\x49\xf0\xbf\x40\xd4\xbf\xb7\xc1\xf5\x7d\x5c\xb1\x20\x15\x02\x9e\xa4\x84\x75\xb7\x21\xe2\x5c\x49\x04\xe2\x2f\x53\x0b\xe3\x82\x05\x29\xc8\x9f\x5d\xa2\x1a\x38\x6b\x2c\x11\x43\xc2\x11\xf0\x7a\x58\x22\xce\x4e\x5c\x81\xe0\x6c\x0b\x00\xa9\x38\x63\xec\xdd\x81\x70\xc0\x2f\xba\x82\x02\xc1\xdb\x54\x1c\x01\x87\x01\xc5\x61\x60\x18\x10\x02\x83\xc3\x80\x10\x30\x18\x65\x0e\x52\x23\x61\x9c\x71\x36\xf2\x78\x7b\x67\x2c\x10\x0c\x00\xc9\xbb\xdb\x60\xf1\x24\x20\x5a\x12\x02\x00\x9d\x85\x3c\x33\xc4\xa1\x10\x24\x00\xa4\x88\x71\xbd\x85\xc5\xd9\x3b\x90\x7e\x7d\x08\x00\xe9\x91\xb0\x2e\x86\x40\x34\xf8\x77\x00\x15\x9c\x33\x16\x0a\x44\x00\xc1\x40\x5d\x80\x9c\xdc\xbf\x2a\x41\xff\xb1\xa1\x0f\x45\xc3\xfe\x97\x03\x0b\xf3\xf8\x7e\xfc\x03\xe0\xe5\x0f\x9b\x32\x14\xed\x1b\x18\xbe\xd5\xd5\x9a\x48\xc8\xb8\x23\x72\x7b\xe0\x46\xe1\x06\x39\x30\x26\x39\xf7\x9a\xbc\xcc\xc1\x5e\x28\x66\x52\x50\xd6\xe1\x5e\x9b\xff\x0c\xfa\x1e\xbf\xe6\xa8\xa7\x49\xdd\xe2\x47\x89\x1c\x2a\x55\xd5\xbc\xbc\x50\x8f\xf1\x97\xe7\xa4\x5a\x72\xe1\x6a\x30\x91\x24\xd7\x5a\xc0\x8c\xf5\x52\x18\xff\xf4\xe4\x66\x71\x48\xdd\xf0\x82\xae\x01\xbf\xcf\x4e\xe3\xc7\xf1\x0c\x6e\x09\x22\xdb\xec\x43\x7e\x90\x61\xb0\x81\x6f\x4e\x78\xdc\x8b\x7d\x8f\x66\xb6\x1d\x0a\x98\x83\x2a\x5b\xa1\x93\x08\x66\xd7\x3d\xf3\xb5\xcf\x12\x8c\x7f\x59\xca\x73\xa8\x36\x69\x32\x6e\xfa\xdb\x75\xef\x19\xda\x81\x4b\x6b\xf0\xd2\x6b\x2c\x17\x8b\xe7\xc8\xc4\x46\x19\x1f\x45\x95\xa7\x1a\x8e\x3a\x69\xde\xec\x0e\x6c\x84\xc7\x25\x63\x98\xb2\xa6\x7c\x71\xab\x79\x31\xcc\xf9\x13\xee\x61\x66\x2c\x41\xee\x8b\xee\xdc\x07\x4c\x6c\x08\x0a\xe2\x07\xb2\x27\x3a\x33\x94\x72\xaa\xb4\x99\x33\xed\x3b\x82\x5b\x8a\x8b\xf0\x2d\x01\xc7\xae\x96\xb8\xf8\x6d\x53\xcd\xf4\x48\x77\x93\xc5\xf4\xb9\x6f\xb5\xe2\x6d\x84\x07\xe9\x81\xf3\xd9\xd6\x95\x9f\x54\x43\x9e\x04\xa5\x3c\x80\x3f\x0e\x9b\x39\x86\x46\x5f\x94\xf0\x6f\x3a\xff\x91\x3f\x32\xe7\xbf\xc8\x47\xf2\xbf\x28\x05\xa4\xe7\x61\x4d\x3a\x33\xf4\x89\x1e\xd8\x5f\xa8\x02\xc6\x1d\xfb\xcb\xf3\xff\x17\x0c\x8e\xe8\x4e\x52\x74\xc0\x10\xcf\x74\xa0\x81\xf9\xfb\x1d\x02\x03\x80\x8c\x70\xb6\x24\x07\x77\x53\x14\x0a\x05\x44\x22\x91\x40\x38\x1c\x06\x84\xa1\x25\x81\x30\x18\x0c\x08\x45\x80\x81\x68\x18\x0c\x88\x40\x20\x81\x50\x14\x0a\x88\x00\x83\x7f\xdf\x08\xe4\x2f\xff\xd9\xbb\x39\xe0\x1f\x0a\x06\xa2\x7e\xe9\x04\xa4\x4f\x30\xc0\xe3\xce\xc6\x0f\x44\xff\x53\x38\x10\xf0\xbf\x2b\x53\x81\x00\x25\xff\xf0\x43\xfe\xc3\x4f\xc0\x9f\x89\xf2\xf7\x9a\xda\x44\x82\x8d\x1e\x96\x64\x0a\xd2\x56\x52\x01\xe9\x63\xbd\x49\xe6\xff\xe3\xb3\x7f\xf4\x4b\x1b\x63\x7f\xf6\x20\x9e\xc9\x1c\xfe\xeb\xa8\x49\x17\xeb\x4e\xf0\x20\xda\x60\xdd\x81\xbf\x42\xe8\x82\x34\xb1\xb6\x38\xcc\xd9\xcf\xe7\x2c\x00\x42\x12\x21\x01\x45\x21\x10\x92\x60\x04\x02\x02\x41\x43\x80\x68\x38\x44\x02\x8d\x84\xc0\x21\x28\x18\x14\x8a\x86\x21\xcc\x41\xaa\x44\x82\x87\xab\x8c\x0c\x48\x0f\xa4\x4f\xc4\xe0\xdd\x5d\xcf\x16\xb7\xf1\x01\x29\xea\x81\x94\xb0\x9e\x38\x1b\xac\xae\xaa\x02\x48\x0d\x48\x22\x7a\x60\xe5\xe4\x40\x8a\x04\x3c\x09\x8b\x27\xb9\x03\xa1\x67\xb1\xfe\x23\x51\xf8\x7f\x49\xd4\x1d\xf0\x47\x76\x80\x7f\xa7\x07\xfc\x3b\xbf\xb3\x94\xce\x5a\x7e\x1b\x67\xeb\x6e\x0a\xfc\xc5\x3a\x33\x15\x09\x1e\x67\x5d\xfa\xcf\x5e\x40\xff\x11\x43\x11\x43\xc2\x38\x13\xec\x7f\xc7\xfa\xdd\x0e\x00\xe8\x8e\x2b\x16\x2f\x6f\x73\xa6\x15\xd3\xdf\x6b\x81\x8c\xef\x9a\x00\xf1\x1e\xce\xce\xbf\x1f\x60\xf3\x33\xb1\xe0\xed\x85\xb0\x78\x71\x03\x3d\xe1\xff\xd1\x6c\xd8\xbf\x03\x28\x12\xb1\x18\x12\x81\x28\xa3\xa2\xac\xa2\x02\x06\x23\x50\x60\x30\x0a\x0a\x06\x23\x25\xc1\x60\x14\x1c\x0c\x46\x22\xce\x6c\xb9\x5f\xd3\xb3\xf5\xb0\xc1\xfe\x5f\x1e\x5c\xf1\x37\x07\x09\xfd\x9b\x8f\x00\x83\xe1\x2a\x60\x30\x12\xf9\xf7\x7d\xe6\x83\xfd\xc6\xa1\x60\x30\x18\x86\x04\x83\xa1\xca\x60\x30\x0c\x2c\x07\xf8\x1d\x12\x47\xc0\x2b\x61\x48\x58\x21\x25\x29\x28\x18\x22\x09\x96\x84\xa0\x21\x48\x18\x1c\x8a\x34\xb9\x2e\xfc\x1f\x99\x7a\x13\xb1\x76\x00\x30\x10\x02\x07\x80\xff\x75\x01\x91\x08\x04\x0c\x01\xb4\x03\xfe\x8d\x21\x25\x25\xd1\xc0\xdf\x1e\x3c\xf0\x5f\x3c\x88\xe4\x1f\x18\x14\x8a\xf8\x27\x86\x82\x20\x51\x7f\xf2\xe0\x7f\xf0\x90\x90\x3f\xd7\x43\x42\xe0\xe0\x3f\x30\x18\x1c\xfa\x07\x86\x94\x84\xff\x81\x49\x9e\xc9\xe4\x9f\x18\x1c\xf6\x47\x7e\x50\x24\xf2\x0f\x0c\x86\xfc\x0f\x1e\x89\x88\xc1\x39\x63\x89\x67\xa3\xd4\xc3\xf9\x62\x81\x10\x38\x48\x97\x40\x20\x01\x7f\x89\x48\x17\x00\x52\xc3\xdb\x11\x80\xbf\x06\x7e\x66\x28\x01\x4d\x81\x32\x50\xa4\x82\x22\x0a\xa5\x08\x81\x28\xa8\x20\x20\x28\x28\x04\x06\x56\x41\xca\xc3\x95\xd1\x92\x48\xa8\x0a\x0c\xac\x04\x96\x03\xfc\xef\x94\x33\xe5\x2a\x11\x6c\x14\x1d\xb0\x36\x4e\xee\x1e\x2e\x40\x10\x1a\xa1\xa0\xa8\x20\x0f\x55\x42\x41\x51\x28\x79\xb4\x3c\x4c\x19\x8e\x52\x02\x4b\xa2\xa0\x2a\x28\x25\x38\x5a\x12\x0c\x03\xfc\xfa\x27\xc1\x10\x49\xbf\x46\x8a\x42\xc0\x60\x00\x01\x01\xe5\x3b\x2a\x80\xff\x13\x00\x00\xff\xff\x25\xb2\x5e\xd5\x50\x1f\x00\x00")
+
+func testAssetsTest_expectedPdfBytes() ([]byte, error) {
+ return bindataRead(
+ _testAssetsTest_expectedPdf,
+ "test/assets/test_expected.pdf",
+ )
+}
+
+func testAssetsTest_expectedPdf() (*asset, error) {
+ bytes, err := testAssetsTest_expectedPdfBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "test/assets/test_expected.pdf", size: 8016, mode: os.FileMode(420), modTime: time.Unix(1568824473, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+var _testAssetsTest_templateOdt = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x79\x77\x50\x93\xdb\xd7\xee\x8b\xf4\x8e\x34\x01\xa9\x0a\xd2\xa4\x17\xe9\xd2\x11\xa9\x0a\x08\x86\x5e\x42\x87\x84\x24\x74\xa4\x57\x81\xd0\x11\x10\xa4\x07\x04\x29\x52\x0f\x20\x20\x4d\x90\xaa\xa1\x0a\x02\x02\xd2\x09\xbd\x48\xbf\xe3\xef\xce\xb9\x47\xcf\xd5\xf3\x9d\xdf\x7f\xdf\x9a\x79\x67\x92\x99\x3c\xcf\xde\xd9\x7b\xbf\x6b\x3d\xfb\x59\xba\x1a\xd8\x38\xd4\x00\x40\x00\x00\x83\x4f\x84\x75\xcc\xde\x09\x93\x70\x02\x00\xf0\xfd\x21\x00\x00\xc0\xc5\xc1\x05\x8c\xf0\x86\x82\x2d\xa1\x50\x67\x07\x6b\x4b\x84\x03\xc4\x55\xc0\xc3\xd5\x86\x1f\x62\x09\x77\x80\xf3\x43\xa0\x60\x57\x1b\x88\xb5\xbb\x0b\xd8\x15\xc1\x8f\x00\x7b\x21\xfe\x2f\x19\x01\x01\xc1\x7f\xc8\x80\x1f\xe2\x3f\x64\x60\x84\x25\xbf\x97\x8b\x73\xac\x9e\x56\xdc\x73\x41\xea\x37\x47\x86\xca\x35\x86\x95\xc7\x04\x16\xc4\x13\xc6\x3a\xa6\x2e\x85\x5a\x85\xcd\x2a\xc8\x73\xc5\xcf\x94\xd7\xe9\xed\x3e\x5d\x9c\x66\x0c\x0d\xb8\xc2\xb8\xe6\x03\x97\x07\x04\x21\x2f\x30\xf3\x73\x98\xcf\xce\x66\x6d\x88\x87\xa5\x68\x62\xec\x0d\x17\x79\x37\xbe\x82\x12\x8d\xad\xf1\xcd\xa6\x6c\x5f\xf2\xd1\x67\x8a\x2c\x71\x89\xd8\x34\x19\xe6\x83\x39\xaf\x02\xf0\xfa\xc4\xb5\x4b\xa4\x40\xd7\xd9\x32\xd0\xd2\x8f\xa7\x1e\x86\x9e\xb4\x90\x9f\x48\x7c\x40\xa9\x5e\x7d\x27\x54\x92\x60\x91\x7a\x5c\x41\x79\x52\x31\xf3\x8a\xae\xb3\xd0\x53\xe8\xa9\xe3\xd8\x7b\x24\x8f\x47\x38\x12\xa4\xed\x4e\xec\xf8\x3a\x67\xcb\x44\xaa\x4e\xac\x0c\xd7\xbc\x6c\x32\xda\x41\xf5\x66\xdb\x55\x51\xc1\x48\x29\x6a\x3c\x36\xd0\x1f\x90\x58\xfd\x80\x4d\x77\x5a\x4a\xc5\x0f\x46\xa6\xc5\xa0\xf5\x14\x09\x84\xd0\xa2\xa9\x15\xbb\x3d\xa8\x62\xc3\xed\xa0\xf5\xea\xd2\xa7\x58\xb6\xa5\xad\xd2\x20\x35\xb3\x5a\x4d\xcf\x1e\x10\x8d\xb5\x95\x6b\xe0\xe3\xee\xed\xe9\xfd\x9c\xa7\xd0\x7b\x7e\x99\xc7\x5e\x4d\xc4\xd6\x8d\x5a\xdc\xb7\xb0\x06\x45\x64\xe4\x70\x1b\xf3\xb9\x70\x8a\x0c\x1f\xce\x47\x1c\xf3\xf7\xdb\x27\xa2\x23\x71\x1e\x27\xaa\x1d\x6f\xd3\xda\xe7\x6a\x95\xb3\x16\xb8\x5f\xdf\x8a\x8e\xa6\xbf\x34\x38\xff\xac\x12\x47\xe8\xfd\x82\xcf\xb5\x97\xc5\xe0\x4c\x00\x92\x53\x3a\xaf\xb3\x41\xff\xe2\x0f\x31\x2e\xa8\x95\x4b\x09\xcb\xae\xc5\x17\x07\x12\x24\x9a\x95\x0c\x1a\xef\xe7\x6b\x73\xb6\x7a\x0c\xbe\x4c\x2c\x3a\x15\xfd\x64\x22\x74\x58\xf6\xb1\x35\xc8\x38\xb4\xfe\x53\xc2\x02\xd7\x52\xa8\x45\x82\x2d\xb7\x86\xd5\xb5\xf8\x4e\xf5\xcc\xa2\x08\x3f\xdf\x48\x45\x55\x80\x20\xdd\xd7\xab\xfe\xf9\xc6\xf8\xde\xd4\xe3\x67\x39\xd5\x89\x10\xd5\xcf\x55\x4a\xd8\x72\xe3\xd7\xc0\x1a\x43\x2e\xc3\x04\xbd\x22\xa5\x10\xf9\x27\x74\x27\xdf\x62\xea\x0c\xfb\xa1\x5d\x24\xa5\xa4\x4e\x3a\x1a\x6c\x19\xc4\xcd\xe2\x56\x6d\xf3\xf4\x0b\x9c\xdd\x4b\xb4\xfd\x20\xa3\x89\x5e\x17\x87\x80\x44\xda\xc6\x54\x37\x3b\x98\x12\x89\xac\x3e\xcd\x4a\xc7\x55\xc5\xa3\x72\xa7\x03\x72\x5d\x0d\x7c\x82\x4a\xe6\x2a\xf1\xcf\x58\x00\xb0\x81\x0d\x00\xbf\x3f\x11\x24\x00\x00\xc0\xc1\x08\x84\x83\xab\x1d\xfc\xfb\xa9\xa8\x05\x19\x1b\x32\xa8\x53\xf8\xcf\x9a\xcf\xcd\x4d\xe7\xbc\xa3\x59\x2c\x96\x0c\x64\xb3\x0c\x69\xc7\x8a\xb1\xdb\x1b\xeb\xa7\xfe\x90\x5b\xe4\xa4\xed\x19\xf7\xb6\x75\x76\x0d\x2f\x26\xda\xa2\x87\xe4\xa0\xa0\xbf\xb0\xa4\xa9\xa4\xa5\x82\x5f\x76\x3c\x7b\x29\x4a\xca\x07\xcf\x9a\x63\xed\x39\x7a\x35\x22\x09\x7d\x75\x85\xc0\xfa\x65\xf7\x9d\xc2\x6e\x85\xbd\x6f\x55\xfb\xab\x87\x15\x4e\x8c\xb1\x36\x38\xa3\x74\xf9\xb7\xb1\x30\x48\x3e\x09\xb5\xb8\x89\x9b\x3e\xa6\x53\x8e\xa1\x3e\xec\xab\x61\xfd\x2b\x3c\x44\xbc\x2e\x06\xd1\x38\xce\xa3\x2a\x06\x9a\x0a\x15\xe6\xa5\x2d\xfe\x8d\x0c\xc9\x7c\x15\xfd\x29\x23\xc3\xcf\xd1\x9d\x0d\x35\x20\xfe\x21\xdb\x6a\xf2\xea\x38\x96\x0f\x31\x7d\x45\x86\x86\x86\x8b\x50\xcf\x73\xae\x1b\x2d\x32\x2b\x60\x01\x43\x5a\x66\x39\x69\x49\x49\x01\x81\x80\x56\x19\x26\x7f\x4c\x53\xe5\x28\x7f\x47\xfb\xa3\xcb\x56\xe1\x81\xc7\xab\x1e\x2e\x11\x07\x87\x90\x0a\x43\xdf\xc7\x17\x92\xa9\x64\x94\x90\xe7\xf9\xa3\xe0\x8a\x0d\x94\xb2\x46\xb9\x9c\x46\x5a\xb3\x51\x59\x0d\xa6\xf9\xb2\x75\x2f\xeb\x45\x05\x3d\x05\xb6\xa6\xc6\xba\xb4\x6b\x02\x9a\x29\xcc\xc7\xa4\x51\xb6\x81\xbf\x95\xe6\x75\xeb\x84\x78\x0c\x63\xc4\x67\x4e\xfa\xa6\xf7\x8f\xda\x8a\x96\x53\xa2\xf7\xac\xda\x5b\x83\x19\x75\xea\x04\x9a\x60\x06\xc7\x69\x3a\x5e\x01\xc1\xd5\xcc\x9d\xd4\x56\x1f\xd2\x08\x64\xf1\x9a\x72\x2d\x22\x8b\xe5\x57\xd0\x65\x22\xf4\x5f\x87\x3f\xa2\xdd\x43\x44\x88\x8f\xe8\x3a\xf8\x57\x6f\xe6\x6e\xcc\xcc\xc7\x51\x25\x30\x6b\x30\x75\x30\xce\xd7\xd7\x4d\x32\x9b\xcf\x31\x3d\x75\xaf\x9e\x18\x9a\x2f\xf3\xb2\x7e\x39\x46\xbf\xea\x82\x51\x3a\xbe\xdd\xca\xe9\x19\x3e\xd3\xf5\x78\x91\xa7\x75\xc2\x54\xfe\xd8\x48\xb2\xb9\x9f\x77\xb4\x37\x37\xb8\x43\x89\x23\x52\x56\xa2\x32\x64\x64\x46\x3b\xd4\xeb\x6c\xf1\x10\x14\x6f\xaa\xb6\x2c\xf8\x09\xb7\x75\xd8\xaf\x2a\x80\xf5\x16\x76\x62\x0d\xbd\x13\x95\x68\x39\xc5\xf5\xda\xae\x2b\x22\xe3\xf2\x29\x1c\x3e\x54\x68\xde\x55\xc9\x67\x67\x1a\x3e\x4c\x6e\x3e\x82\x9a\xad\xb5\x1a\x7a\xca\x64\x03\xc8\x5a\xf3\xa5\x1a\xad\xbe\x43\x05\x85\xaf\xac\xae\x0c\x8d\x6f\x2b\xf7\x33\x90\xf6\xb1\x14\xe4\x16\x9b\x52\xc9\x77\xd5\x88\x08\xf7\xad\xaf\xf2\xc1\xb8\xa6\x60\x3b\x62\x09\x98\xc6\xe6\xf9\x77\x18\x72\x5d\x86\x99\xbb\x6e\x02\x02\x36\xaa\x12\x3c\xd8\x17\xec\x21\x9e\x51\x7c\xdc\x17\xf6\x59\xc5\x74\xb0\xfb\x14\xb3\x34\xd6\xc5\xd1\xf6\x52\xe2\x69\xab\x5a\x2d\x72\xc9\x89\x19\x63\x2b\xed\x87\xf5\x9d\x3e\x42\x39\x78\xc7\xe6\x31\xf7\x38\x85\xe7\xab\x12\x96\x21\x10\x4e\x71\x16\xba\x1a\x6e\xac\xa7\x58\xea\x0a\xfe\x39\x88\x41\x52\xd1\xae\xb3\x39\x3e\xaa\x57\x55\x4f\xbc\xb7\xee\x69\xb6\x89\x92\xde\x7a\x02\xfd\x64\x6b\x2b\x9d\xc6\x27\x53\x88\x48\x89\xb4\x08\xe6\x09\xd7\x72\x94\x09\xcd\x18\x52\x5f\xa3\xf5\xec\x40\xe1\xd0\x04\xa4\x08\x4d\x94\x0b\x06\x87\xb0\x0f\xeb\xf3\xbd\xd9\x78\x1d\x77\xe1\x6b\x89\xd2\x12\x8d\x82\x62\xfb\xf5\x24\xb2\xe1\x4c\xc1\x71\xa9\xbf\xa1\xf0\x3c\xd0\x2a\x58\xc7\xd8\x42\xfb\x31\x3a\x53\xf4\x1a\x03\xf3\x59\x6c\x9d\x62\xbc\x61\x9b\x6a\xaa\xa8\x72\xf5\xfd\x5e\x20\x3a\x07\xa5\x95\xdc\x5d\xba\xa4\x0d\x52\x70\x37\xb0\x13\xdd\xdd\xde\xcc\x4b\x79\x61\xe3\x2d\xc3\x52\x37\xf2\xd1\x71\xe7\xde\x8b\x00\xcf\xbb\x2a\x29\x50\xc2\x2d\xbb\x5b\x36\x92\x43\x58\x3a\x38\x19\xfe\x37\x92\xad\x3e\xec\xf1\x66\xfb\x78\x9d\x40\x5b\x44\xac\x68\x13\x2f\xe6\x52\xc4\x92\x35\xf5\x22\x9b\x84\x61\xe9\x9f\x06\xb6\x71\x45\x45\x69\x88\x96\xfd\x32\x65\x2d\x28\xa1\xef\x73\x17\x1f\x90\xbf\x54\xc8\xc6\x13\x73\xba\xb0\x8a\xae\xbc\x68\x47\x93\x96\x4b\x18\xbb\xa5\x55\x80\xd2\x29\x37\xdf\x3e\x8f\xe2\x7d\x7e\xf4\x7e\x97\xe8\xa2\x8d\x8d\x18\x4f\x76\x89\xe1\x46\xad\xa9\xe0\xd5\x94\x2f\xca\xc6\xd7\xae\x31\x56\xca\x17\xde\x23\x0e\x75\xfe\x0c\x14\xce\xf4\xb5\x1f\x93\x8e\x55\x0b\x6f\xb2\xb9\xa6\xba\xda\xa8\x38\x56\x5e\xb4\xc8\x34\x15\x9b\x50\x6f\xe3\xa4\x95\xd5\xc6\x57\x42\xdd\xcc\x9e\x81\xd2\xba\xe4\x46\x0d\x42\xec\xe3\x6e\x3f\xe1\x24\xe8\xd8\x77\xc4\x4e\x52\x7e\xc3\x7d\x70\x24\x30\x26\x33\x37\xe2\x6c\xad\x78\x58\xc0\xfc\xe1\x95\x6f\x2d\xa7\x52\x5c\x06\x0a\x14\x8a\x8a\xbf\x89\xd8\xdc\xc6\x62\x0a\x43\x2e\x4b\xea\x14\x67\xcd\xe9\x4c\x7d\x94\xdb\x71\xeb\x1c\x7a\x94\xef\x92\x2e\x82\x63\xb8\xec\x6c\x36\xc9\x35\xcf\x1c\x49\xc7\x5d\x0a\xa3\x7f\x85\x3f\xe2\x76\xf2\xa5\x42\xd2\xee\x76\x4a\x64\x67\xf9\x8b\x6f\x12\x95\x18\xda\xce\xfb\xe4\x5e\xf8\x37\x13\xb8\xc9\x48\xc5\x38\xba\x33\x7a\x2a\xb6\x04\x4d\x0d\xa5\x23\xf0\xdd\x8d\xd7\x56\xad\x19\x88\xeb\x5d\x3e\x83\x9c\x77\xa9\x4c\x76\x75\x6c\xb3\x18\xf9\xee\x91\x4e\x70\xae\x16\x7b\x8c\xa7\xeb\xba\x67\x5f\x18\xf1\x78\x68\x36\x97\xf8\xf3\xdc\x08\xf6\x0a\x50\x94\xcf\x9e\x44\x89\x8e\x1d\xf7\x45\xdc\x31\xc7\x16\x7b\x7d\x23\xcd\x3d\x7d\xf4\x63\xa8\xfe\x2e\xad\x0e\xaa\x2c\xec\xf0\x4d\xa1\xb5\x22\xaf\xca\x91\x86\x92\x8d\x71\x23\x1f\x9f\x1a\x45\xb9\xed\xb9\x8d\xa1\x9a\x96\xb4\xca\x97\x3b\x5e\x21\x33\x77\x0b\x56\xd4\x64\x8b\x6a\xd5\xb6\xb1\x49\xfc\x5f\x0c\x23\x95\xfb\x27\x5a\xce\xcb\x19\x06\xb6\x7c\x1c\x49\xfd\xa8\x4e\xfa\x8e\xb8\xc7\xc5\x83\x31\xcd\xfc\xc8\x87\x88\xe6\x68\xc3\xca\x39\x1d\xa1\x0b\x08\x8b\x08\xd5\x62\x76\xfe\x08\x75\x4a\x66\x5b\x97\x7e\xb3\x58\x28\xd2\xc2\xce\x37\x69\xb6\xfb\xdc\x79\xc5\x6f\xad\x48\x1f\x32\xfe\xbe\x95\xd6\xdf\x8d\xc2\x9f\xd1\xa7\x38\x85\x0a\x8f\x8e\xc8\xdc\x7c\x50\x08\x47\x60\xf5\xc6\x46\xd0\x47\xc5\x73\xbc\xc0\x30\x84\x1f\xfe\x82\x0d\xbb\x7e\x71\x52\x31\xbc\x68\xbf\x5f\x31\x36\x59\xfb\x51\xe1\xd9\x2e\xc1\xcd\xaf\xcf\x32\x48\xb1\xcd\x51\xfa\x70\x81\xcf\x05\xda\xb9\x1b\xab\x42\x05\x8e\x01\xc2\x83\x56\xca\xb6\x0b\xb5\xce\xc6\x24\x0a\xd3\x65\x64\x47\x92\x9a\xe0\xec\x0f\x9d\xfa\x8f\x50\xd8\x37\xd9\xe9\x9d\x4d\x0c\x83\x77\x85\xaf\x2d\x87\x58\x66\x57\xd4\xbd\xcb\xf8\xe3\x4b\x61\xa5\x11\xea\x6b\xea\x52\xa6\xaa\x87\xf0\xb2\x75\x19\xe9\x50\xce\xc5\x01\xb6\x61\x9f\xdc\x29\x8e\x9e\xfd\xc8\x74\xd5\x6c\xa4\x65\x3b\x50\xf5\x7c\xb5\x07\x3c\xdf\xcc\x50\xf7\xcd\xaa\xf9\xda\x99\x1d\xcf\xd7\xa7\x8b\x51\x0b\xc8\x9b\xc7\x51\x8c\x3a\x48\x52\xa3\x3c\xa2\xd6\x9c\x2b\x52\xdc\xc8\x7c\xee\x05\x7b\x15\x05\x72\xbb\x59\x40\x42\xd1\xeb\xf1\x81\x6f\x3c\x09\xab\x3a\xcd\xf9\x50\xfc\xdd\x2e\x11\x92\x5b\xd4\xa8\x0c\xe8\x90\xa8\x37\x92\xcd\xec\x19\x4d\x58\xde\x68\xdc\x9d\x3a\xa4\x38\xff\x10\x4f\xca\x5e\xe6\x2c\x28\x9e\xdc\x85\xbc\xe7\x8f\x44\x92\x20\x8d\x0c\x8b\xaa\x90\x65\x43\x5c\xf8\x18\xbd\x8c\x41\x34\x13\x4d\xc5\x17\x0f\x72\x9b\xa9\x69\xfc\x84\xb0\x4f\xa3\x53\xe5\xa8\x05\x70\x79\xce\x8d\x51\x2a\x17\xa5\x62\x2e\x3d\x13\x19\x34\x7a\x48\xbf\x72\x84\xfb\x71\xd0\x5c\x69\x98\x2c\x57\x0b\x17\xe3\xd3\x67\x99\xeb\xcf\xa1\x75\xce\x7f\xc8\xb9\x61\x48\x27\x4d\x78\xda\xbf\xec\x4c\x2b\x8d\xe7\xd9\x05\x50\x7b\xbd\xcb\xcf\x74\x20\x39\x33\xa0\x8c\xb5\x9f\xed\xd9\x19\x3d\x08\x1a\x34\x7c\x60\x00\x7f\xc2\x49\x82\x4d\xf5\x6d\xe2\xa4\xe5\x8c\xa0\x3a\xff\x3a\x11\x5f\xdf\x1b\xa8\x2c\xf9\x41\xb2\x92\xf7\x88\x93\xf4\x37\x43\x75\x8d\x92\xb9\xdb\x6e\xed\x2f\xdd\xf9\xe0\x7f\xf4\x87\xc6\x50\x05\x0f\x6e\x73\x62\xc5\x1f\xf5\xa0\xa5\xbb\xdc\x36\x6d\x35\xe8\x2e\x1b\x12\xb3\x58\xe8\x52\x66\xfd\x78\x4d\x66\x0e\x68\x0e\x30\x01\xdf\x8b\xd9\x2b\x92\x0c\xbe\x24\x3c\x00\x88\xe0\xfb\xb3\x98\xfd\xa9\x95\x7e\x2c\x66\x8c\x00\x00\x28\x41\x5c\x6d\x1d\xec\xdc\x61\xff\x51\x49\x70\x61\x01\x4b\x6b\x6b\xb0\x33\x18\x66\x89\x80\xc0\x04\x7e\x8f\x64\xf8\x05\x12\x8e\xb0\x44\xb8\xc3\xad\x2c\xff\x5b\x1c\x14\x02\x75\x87\xba\x80\x5d\xdd\xff\x01\xc7\xf2\x0b\x9c\x83\x8b\xa5\x1d\x18\x2e\xa0\xe8\x80\x70\xb1\x84\xc2\xff\x01\x4c\xf7\x0b\xf0\xf7\xf1\xfe\x79\xaa\xbf\x42\xd9\x3a\x43\x2c\x11\xe0\x7f\x42\xfd\x6a\x49\xa1\x30\x88\x1d\x0c\x0c\xff\x1f\x96\xe6\x57\xe3\x21\x20\x10\xe7\xff\x7e\x41\xbf\xa3\xa0\x96\xae\x60\x67\x81\x7f\xd6\x31\x2e\x96\xae\x0e\xb6\x60\x38\x82\x1f\x66\x63\x3b\x90\x34\xe0\x1a\x22\x48\x11\xba\x25\x13\x06\x1e\x9c\x90\x17\xc0\xc5\x85\x71\xf5\x36\x38\x66\xbf\x15\x0b\x37\xc2\x15\xd8\xcd\xa0\x9c\x69\x41\x26\x3d\x7a\x50\x50\xb2\x96\x3d\xb2\x50\xae\x2a\x32\x8b\xac\x79\x8e\xb1\x64\xec\x89\xae\x2a\x6e\xd3\x17\xb5\xe6\xe6\xe8\xbd\x67\x5f\xbb\xca\x2b\xd5\xc4\xdf\x50\x38\x25\x35\x39\x51\xc7\xcb\x92\xc1\xb4\xe3\x18\x52\x98\xcd\xd3\xe8\xe9\xca\x23\x69\x9e\x1a\xc6\x77\x87\xc4\x79\xa8\x3a\x8f\xcb\x2b\x63\x4f\x46\xe7\xcd\x8d\xb6\x51\x09\xb7\x7b\x24\xef\x6a\x90\x04\x84\xcc\xb1\x8a\x0e\x07\x5f\xfd\x18\x24\x07\xe7\x09\x6d\x0d\x1f\x20\xd6\x73\xe2\xbd\x8f\x58\x65\xa3\xa7\xa5\x5e\xe6\x54\x11\x59\xd6\x3d\x6c\x8e\xa8\x63\x4d\x08\x92\x8f\x25\xd4\xec\x0d\x45\x84\x30\x36\x6a\x6b\x74\x89\xa4\xee\x3d\x90\x6c\xcb\xc5\xcc\x77\x5b\x34\x0f\x87\xe4\xc9\x58\x6c\xf3\x22\x87\x26\x66\x22\x49\x50\x8b\x66\x5f\x9a\x2b\x37\x4d\x42\x66\xfd\x52\xec\x7d\x36\xad\x7d\xdc\x87\xbb\x76\x66\x6f\x26\x5b\x64\x0a\x6f\xea\xac\x35\x49\x7b\x5f\x2e\x5f\xf0\x0f\x5d\xd2\xcf\x57\xd4\x78\x28\x3b\xcb\x0f\xe6\xdb\x9d\xc4\x2f\xd7\xec\xb2\x18\xa8\x55\x7c\x7f\x73\x6a\x8e\xec\xd1\xb8\x58\x00\x10\xf2\x8f\x32\x90\xe8\xbb\x0c\x44\x78\x3b\x83\xff\x23\x02\x31\x20\xe3\xb8\x29\x71\x5a\xff\x59\x73\xcb\xe2\x95\x96\xf7\x89\x1b\xe9\xfd\x13\x03\x05\xe4\xb8\xb4\x7c\xf1\x88\xa0\xac\x7d\xcb\x6c\x22\x50\xde\x6d\x57\xae\x87\xac\x2f\x5f\x67\x99\xb7\x46\x50\x73\x3f\xa8\xb2\xe1\x4d\xaf\x69\x8a\x76\x27\x62\x17\x1b\x9a\x6b\xf0\x5b\x6e\x7f\x3e\x73\xe1\x75\x7b\x75\x1e\x97\x58\x5d\xc2\x97\x28\x11\xcc\x78\x5b\x50\xc3\x5b\x75\xe2\x82\xfb\x1b\x66\x61\xb6\xbd\xfb\x4b\xb7\x67\x80\x77\xdf\x5e\x66\x81\xd7\xf3\x9d\x1a\x7e\x21\xbd\x5c\x7d\xce\xa7\x8b\x57\x6e\x89\x34\xb8\x44\x66\xee\x15\xe5\xe8\x96\x4c\xaa\x5e\xad\x10\x74\x52\x7d\x66\xb2\x91\x6f\xc0\x08\xc9\x5b\xf2\xb4\x58\xde\x50\x49\xfd\x98\x7a\x09\x95\xb8\x4d\xee\xd4\xdd\x84\x3c\x7c\x10\x4e\x4d\x64\x4e\x20\xe7\xb7\x6c\xcb\x48\x6c\x80\x1c\xbc\x27\xfe\x5c\xcb\xa6\x18\x29\xb6\xb0\x18\xba\xc2\x7e\x05\x49\xd1\x1a\x48\x8e\x76\xeb\xd3\x8f\xbf\x2f\x8b\x5b\x6b\x61\x8c\x64\x4f\x30\x0e\xbf\x7f\xbb\x9e\x67\xda\xf1\x6e\xe6\x90\x71\xb6\xaf\x53\xbc\xa3\x2a\x8c\xbf\xb7\xa1\x5a\xac\xe2\x6e\xbd\x72\x49\xda\xf6\xaa\xd0\x09\xa0\xbc\x37\x1e\x71\x54\x66\x77\x85\x89\xff\x4a\x56\x67\xc2\x92\xdb\x19\x77\x1e\xb7\x9c\xd4\x72\x67\x4b\xac\xc5\xa0\x13\x1e\x9d\xd2\x48\x59\xf3\x37\x23\x4b\x05\x51\xc2\x07\x22\x2e\x53\x49\x5f\xae\x97\x7d\x40\x9a\x91\xd6\x5b\xc2\x44\x02\x25\x9b\xdf\x3e\xb9\xe6\xc0\xc7\xbc\x37\x77\x63\x4f\x81\x22\x4c\x65\x3d\x2f\xd9\x49\x2c\x7c\xae\x3b\x40\x39\x83\x96\x47\xf0\x2e\xec\xf3\x2b\x7e\x4f\xa3\x7e\xd2\x67\x83\x8a\x49\x6b\x76\x9b\x43\x29\x16\x69\xe7\x9f\xf2\x1e\x5e\xcb\x6c\x8a\xc3\x29\xff\x43\xb7\x0c\x8e\x4b\x59\xad\x37\x2c\x78\xda\x88\xb8\xd5\xef\x41\xe3\x95\xbb\x72\x3a\x3c\xfe\xe9\x6c\x75\xfd\x15\xfe\xc3\x67\xee\xe3\x42\x0c\x5f\x9f\x29\xf8\x79\xc8\x92\x75\xc0\x0b\x42\x9f\x64\x6c\x52\x65\x4c\x3e\xe2\x6f\x1b\xf1\x9e\x08\xb7\x7a\xc5\xcc\xca\xd1\xf1\x30\x3c\x54\x61\x66\xc7\x71\x7d\x60\x93\x46\xe4\x91\x7d\xf9\xa7\x8c\xfa\x08\x77\x43\x16\x03\xbe\x62\x21\x09\x8d\x9b\x9a\xaf\x5f\xb0\xe7\xdb\xa4\x2b\x9b\x76\xa4\x97\x2e\x72\x39\xa4\xed\x09\x2e\x82\x71\x6b\xe5\x7a\x94\x7b\x5b\xf3\x44\x8c\x62\x64\x08\x5b\x7a\xfa\xb4\xe6\x5c\x2b\xf1\xb6\xfd\xca\x59\x64\xf0\xf9\x4a\xc5\xef\x9a\x1d\x56\x57\x2f\x80\xef\xce\x3d\xb1\xee\xf5\xc3\x8e\x99\xb2\x6d\x39\x0f\x80\x7c\x34\xc1\xdd\xfa\x4c\x94\x0e\x37\x62\x82\x27\xcf\x0b\x76\x70\xa7\x6c\xcd\x88\x1f\x7f\x22\x13\xc8\x98\x05\x8d\xeb\x6d\xb7\x68\xab\x87\xd3\x0e\x45\xbe\x67\x37\x0a\x2c\x43\x56\xb0\xef\xef\x53\x9f\xd1\x18\x98\x5c\x7c\x11\x12\xc3\x6e\xcb\xe3\xd4\xdb\x0b\xf7\xed\x67\x55\x8d\x57\xda\x95\x2f\x20\xc9\xe3\xf5\x21\xdc\x97\xa7\x9a\xef\x12\xd1\xec\x2b\x72\x27\xa7\xb3\xa3\x63\xf5\x8a\x32\x8a\xcb\xbf\xc1\x13\xa6\xb2\x89\x4b\x68\x35\x16\x2a\x20\x48\xe6\xf7\x02\x3c\xf5\xcc\x62\x8f\x61\xb8\x51\xd7\xaf\x14\x9a\x55\xd0\xbe\xe8\xe4\x7c\x22\xff\xb0\x8d\x63\x68\x74\x3e\x7b\xc5\x4e\xf3\x16\x86\x61\xfb\x9c\xbb\x4e\x36\x69\xb7\xc1\xf6\xe6\xad\xda\xd0\x71\xc7\x38\x54\xef\x20\xf2\xce\xa9\xbe\x1d\x38\x1c\xae\xaf\xd7\xe4\xeb\x9e\x58\xa6\xee\x1b\x54\x12\x5c\x10\xb4\x50\xdb\xda\x41\xc4\x20\x47\xfb\xf4\xad\xab\x61\x13\x5b\xca\x97\x6c\xc1\x7d\xea\x5e\xcb\x6b\x3b\xd3\x27\x9b\x03\xb2\x8d\xc4\xb4\x3e\x07\xa1\xb8\x41\xe8\xd4\xdd\xca\xc9\xaf\xdc\xfa\x8f\x06\xeb\xf1\x29\x68\x76\xef\xa7\xbb\xb6\x79\x15\x07\x4c\x71\x57\xc9\xd3\xdf\x0d\xb5\x28\xeb\xe4\x91\x91\xc3\xdd\x81\x65\xa5\xe6\x77\x0e\x07\x16\x5b\xbc\x26\xb7\x14\xeb\xc8\x7d\xa5\x4b\xa4\xe1\x69\x04\x55\x90\x83\x4f\xa3\xc4\xd6\xd7\x76\x6e\xec\x45\x7b\xf0\xc8\xda\x85\x8d\xdd\x31\x5c\xa3\x5f\x8f\xd4\x24\x16\xc6\xbd\x4b\xa6\x4c\x63\x59\x2d\xd0\x73\x35\xc5\xec\x50\xff\x1e\x6b\x88\x27\xb9\x8b\x12\xfa\x8c\xad\x84\x66\x5c\x8a\xb5\xf0\x5e\xb8\x77\xc7\x53\x7b\x03\xd6\xa2\xb7\x69\xaf\xa9\xe8\xda\x7a\xc9\xd9\x6e\xbc\x5c\x03\x93\x59\xd2\x8f\x69\x99\xb2\x98\xfa\x45\x67\x71\x0c\x52\x4c\x94\x29\xc0\xb6\xef\xaf\x8d\x64\xd4\x95\x81\xc7\xdf\x65\xa5\x9b\x68\x90\xb4\x08\x33\x49\xe5\x7f\xb6\x0d\xa5\x14\xdb\xa4\x17\x1b\x8d\x66\xb3\x26\x31\x4d\x1c\x92\xd7\x79\x94\x22\x6c\x4d\xfd\xfa\x49\xec\xa1\x60\x68\x3a\xfd\x3d\xcb\xdc\x47\x57\xd0\xf3\xf5\xcb\xb4\x78\xaf\x2c\x22\x28\xfd\xdb\x83\xe9\xb7\x54\xdd\xc3\x2b\x27\xd9\xc4\x41\xa1\xa0\xc2\x9b\x9c\x3b\xbb\x2d\x8e\x96\x88\x49\xa6\x03\xf8\x1c\xd7\x18\x89\xda\xb7\xd5\x71\x78\x98\x0f\x2e\xea\x9b\x84\x73\x0e\x76\x33\x9f\xe1\xc2\xae\x3c\x44\x83\xaf\x47\x17\xae\x71\xb2\x38\x9e\x43\x6c\x52\x31\xb1\x8c\x08\x14\x47\xb7\x75\xbd\xc1\xf9\xda\x10\x38\x62\xbd\xcb\x6d\x24\x76\xa0\xc5\x3a\x86\xa9\x98\xd7\xa7\x49\x4b\x8f\x0c\xa7\x06\xd3\x77\x76\xd0\x39\xde\xcf\x4e\x4c\xac\x69\x19\xe8\xc7\x94\xd8\xbe\x2c\xb9\x1e\xd4\xe8\x0e\xe0\x00\x44\x9d\x47\x3e\xbb\x4d\x7e\x64\x22\x57\xf8\x02\x1d\x38\x2a\x8d\x1b\x74\x1a\x7b\x47\xec\xb8\x4d\x1b\x39\x86\x2a\xc3\x2d\xb6\x52\x16\xf9\x9a\x2f\xd7\xd1\x53\x3a\x82\x1b\x8d\xe3\x82\xeb\x38\xfc\x57\x2d\x73\xe5\xc8\x1a\x47\xa4\x75\x06\x4a\x52\xbb\x02\x17\x69\x6a\x87\x33\x0f\x9b\xc5\xef\xbf\xce\x96\x1c\xe0\xf6\xea\x0e\x2b\xc7\xcb\x72\xac\x54\x96\x9f\xd1\xd5\x57\xcf\xea\xae\x30\x60\x24\xf2\xa7\xe3\x8a\x48\x50\xf8\xe8\xdf\xfe\x07\x11\xcd\xac\xb5\xc1\x0a\x31\x60\xea\x39\x4e\x7d\x4a\x84\x41\xe2\x95\x70\x29\xdd\x20\xbe\xe5\x1b\x41\xcc\xac\x42\x09\x0a\xe6\xcf\x2d\xcf\x19\x5c\x3c\x8f\x79\xb0\x64\xb0\x5b\x20\x41\x67\x5f\x31\xdd\xee\xa6\x97\x56\xfe\x28\x7b\xee\x26\xc9\x59\xcb\xd3\x80\xd9\x9d\x0d\x51\xdb\xb1\x5a\x7e\xc3\x1b\x58\xa6\x78\xac\xa5\xfb\x3e\xfe\xf5\xc2\xeb\xde\x77\x5a\x16\xee\xaf\xfa\x8a\x1b\xad\x25\xa5\xdc\x12\x2b\x6a\xa3\xa6\x64\xbe\x28\xbb\xca\xc4\x5f\xe5\x42\xd1\xaf\x1a\x98\x1e\x0b\xe4\xe1\x23\x12\xc7\x41\x83\xca\x77\x85\xc0\x4b\x93\xef\x59\x02\x24\xe8\x3c\xca\x9b\xee\x6b\x89\x67\xef\x99\x1e\xa7\xdb\xf6\x1d\xbb\x37\xc1\xef\xf8\x3a\x3e\xdb\x58\xc9\xda\xd1\x73\x81\x8e\x44\x97\x32\x0d\x80\x86\x77\x9f\x1c\x7f\x2a\x7a\x2e\xbd\xaa\xd4\x27\x84\x51\x19\xd7\xaf\x65\x31\x95\x34\xe7\x5e\x78\x8d\x0e\xe4\xa8\xca\x5b\xf9\x4a\x33\xd4\xce\xd9\x8a\x12\x6d\x6d\x5c\xbb\xc5\xbb\xb9\x69\xe9\xda\x95\x3e\xa0\x10\xf4\xbe\x45\xd7\x67\x35\xaa\x35\x3b\x0e\xed\x3b\x21\xea\x17\x7b\x7a\x8d\x81\x7c\xf4\x86\x1d\xb5\xaf\x80\x8a\x21\x5c\x4f\xf4\x78\x01\x36\x62\x5a\xda\xaf\xb4\xc9\x69\xa3\xed\x3b\x2b\xe8\x09\xf0\x04\xb5\x54\x82\xa7\x5a\xce\x32\x52\x5d\xca\x6d\x9f\x4b\xc3\xe1\xd0\xc4\x27\x4f\xd5\xf9\x3d\x0b\x99\x75\xea\x77\xa2\x64\xcd\x45\x50\x76\x6f\x97\xae\x91\xa5\x41\xcf\x5e\x64\xe1\x90\xda\xa6\x4a\x09\x1f\xdf\x4d\x65\xdf\x13\xf7\x7f\x41\xce\x37\xed\x23\x24\xdd\xe7\x1f\x84\x8d\xd9\xb9\xbe\x44\x27\xb4\xcb\xbf\x34\x71\xa7\x37\xe2\xb2\xf9\xfe\x9d\xcd\x1d\x90\x58\x3d\xbe\x5d\xee\x37\x5f\x25\x42\xbc\x18\xea\x56\x0b\xc7\x4f\xd7\x03\xf7\x01\x89\x49\xd7\xc9\x35\xe1\x11\x48\x30\x41\x06\x3c\xfa\x8d\x6b\xa2\xc7\x1a\x85\x4f\x76\x27\x87\x90\xb4\x77\x64\xe1\x4b\x63\x67\x72\x37\x6a\x4c\x20\xc9\xcb\x1a\xbb\x13\x3c\x5f\xcd\x8d\xc0\xae\x5e\xf1\x10\x49\xe7\xbe\x63\x2c\xbb\xb9\xcd\x91\x1a\xa7\x09\x1d\x5f\x68\xcc\x24\xde\x08\x77\xbd\xdd\x95\xaa\x46\x2c\x34\xbe\xcd\x13\xec\x02\xf2\x9e\x69\xd2\x78\x39\xd6\xa3\x3b\xf2\x27\x81\x6f\x1a\xb0\xd0\xf8\x5f\xe7\xf0\xee\xb1\xc4\x77\x5f\x67\x7d\x22\x2f\x7e\x47\x1e\xef\x7a\xf5\xb7\x4b\x82\x04\xb9\xf7\x11\x51\x6e\x61\x1c\x1b\x51\x8f\xb5\x0f\x6b\x9b\xd5\x9c\x08\x73\x65\xf9\xbf\x66\xf2\x9e\xfb\x92\xbc\xee\x64\x26\xc8\x88\xe8\xbe\xa6\xec\xf4\x8a\x1c\x67\x20\xe7\x39\x97\x71\xc1\x46\x5f\xc8\xdd\x22\x49\x95\x95\x22\x42\xbe\xa1\x27\xc8\xdc\xf3\x2c\x08\x9b\xed\xa7\xe4\x20\x9b\xcf\xf2\x9c\x66\x37\xd1\x8f\x05\x51\xb6\xbb\x24\x23\x32\xe3\x19\xbc\xc3\x03\x28\x5f\x7b\x7a\xa5\x6e\xa2\x55\xdd\x1c\x09\xc3\x54\x55\x23\x9e\xfe\xfa\xbb\x3e\x85\x47\x66\x8d\xb1\x35\x9b\x41\xc6\xe6\xd6\x21\x9a\x12\x6d\x12\xc1\xd2\xcd\x18\xb6\xc9\x15\x9c\x8f\x98\x00\x7e\xdd\x14\x75\xc1\x76\x51\x89\xb2\xeb\xbd\x41\x36\x27\xeb\x9d\x06\x55\x31\x0b\x9d\x33\xec\x74\xc1\x4d\xda\x20\x97\x54\xf2\x24\x66\x10\x0d\x90\x43\xd4\xc0\x7a\xb2\xc7\x76\xdd\x91\x3f\x59\x5e\x19\xb7\xe3\x7a\x74\xd4\xd4\xd3\xb6\xca\x77\x2b\xce\xba\x46\x21\x28\xe6\xba\x62\x82\xd1\x72\x5f\x84\x10\xf3\x16\xee\xfb\xc9\xe5\x13\xd1\xb1\xcb\x58\xb7\x09\x8a\x0a\xd5\x28\x69\x76\x69\x89\xf9\x45\xa5\x33\x54\xf5\xdb\x65\xaa\x83\xcd\xaf\x3b\x65\x5b\xe3\x0b\x7e\xb1\xb5\x31\x22\x32\xdc\xb8\xa0\xa0\x0b\x39\x34\xea\xb3\x64\x92\xc2\xd9\x95\xe9\x19\x38\x99\x84\x89\x56\x38\x87\x94\x47\x78\x70\xc2\xf4\x35\xe6\x54\x85\xf9\x5e\x85\xcf\x5f\xd5\xde\xc4\x24\x75\x0f\x32\x1d\x8d\x10\x84\x2b\x8a\xd3\xf9\x0f\x24\x75\xbe\x27\x20\xef\x3d\x08\x99\xa1\xd3\x18\x4f\xfa\xa2\x39\xc8\xed\x20\xc0\xab\x7b\x58\x01\x2a\x41\x09\x67\xf8\x38\xbc\xc3\x15\x15\x7f\xf6\xfa\x68\xd6\x33\xce\x5e\xcb\xcf\x7a\x8f\x73\x7b\xf6\xa0\x1b\x36\xeb\x6d\x6c\x51\x90\x5d\xd2\x1f\xd7\x13\xf1\xf6\xba\xcc\xb5\x11\x83\x55\xef\xc7\x20\xc5\x28\x1b\xde\xfc\x51\x5b\xfc\xda\x82\xe8\x0a\xad\x96\x01\x33\x49\xdf\x69\x4b\x56\x39\x86\x4b\xd7\xd3\xe9\x56\xac\xef\xaa\xc3\x7f\xfe\x50\x4e\x8e\x00\x00\x8e\xf8\xfe\x49\x75\x10\x03\x00\x60\x0d\x71\x45\x80\x5d\x11\xdf\x65\x47\xb3\xd1\x03\xc8\x94\x38\x85\xff\x6c\x05\x85\xa8\x05\x44\x98\x6f\x8a\x8f\x91\xcf\xc3\x3c\xdc\x0a\x9b\xd2\x22\x4d\xd6\xe2\x09\x0f\x5d\x54\x62\xd4\x83\x02\xd5\x87\xa9\xcd\x79\x97\xcd\x6a\xf7\xf3\xc0\x5d\xb0\x64\x9f\x3e\x0d\xca\xdd\x96\xa6\xd3\x4d\x2f\xcf\x02\xd8\xe5\x5c\xbc\x08\x44\xa7\x20\x2f\xb8\xe2\x0c\x2a\x7c\x20\xdb\x6e\x9d\xa0\xfb\xa2\xec\xec\x6e\x3d\x4b\x48\xe1\xa5\xe5\x79\x47\xf3\x3d\x3d\x86\x24\xae\xab\x6e\xe9\x54\xe5\x21\x46\x50\xd2\x56\xd9\x0f\x4b\x3c\x25\x85\x8a\x91\x29\xad\xc3\x8d\x91\x14\x94\xb8\x73\x07\x9b\xca\x3d\x24\x03\xa8\x61\x69\xfd\x8a\x73\x55\xce\x24\xf5\x76\xc6\x96\x92\x12\x75\x31\xc8\x6b\x78\x72\x2e\x8e\x13\xe3\x06\xf7\x4b\x7f\x67\x31\x32\xe9\x2c\xf3\x11\x82\xad\xcd\xcd\xf8\x14\xa9\xe2\x3c\x6c\x94\xd2\xfe\xf8\x42\xc7\xad\x99\x04\x46\xfc\x64\xde\xd8\xe3\x28\xb4\x02\x87\x80\x93\x24\x94\xc7\xa6\x1c\x6f\x5b\x44\x3f\xda\x18\x7a\x63\x57\xa1\x2f\x90\xa0\x87\x39\x71\xf5\x03\x4f\x6a\xcd\x07\xde\xd6\x64\x19\x4a\xa3\x07\x34\x6e\x99\x54\x34\xab\x12\xc2\xde\x2e\xe4\x6c\x6b\x79\x37\x7d\x11\x07\x48\x37\xd9\x47\x53\x16\x4d\x8d\x92\x63\x4d\x41\x8c\x02\x1f\x6f\x12\x60\xe2\xba\xf1\xaf\xa5\x77\x8e\x8e\x86\x90\x17\xf5\x58\x80\xfc\x62\x1e\x6b\x92\xb9\x40\x85\xe1\x1c\x29\x4f\x13\x3b\xd2\x95\xe8\xc5\xfd\xde\xe7\xb2\x26\x32\x5c\xb5\x7d\xc4\x27\x55\xfd\x81\xa3\x6c\x87\xca\xf7\xf5\xd9\xed\xe2\x51\x00\xea\x87\xdb\x2c\xff\x4e\x03\xdf\xc4\x14\xd7\x30\xd1\xf1\x86\x69\xe4\xfd\x81\x83\x42\x46\xd7\x4b\x79\x55\x5a\xfb\x9c\x38\x90\xa8\xc0\x1e\xe7\x8d\x6e\xe6\x67\xab\x44\x85\x71\x31\x32\xc4\x01\xaf\x70\x89\x37\x1d\xfb\x14\xcd\x09\x37\x99\x12\x0b\x28\x1e\x36\x7d\xf1\x30\x42\xf2\x6b\x11\x6f\x9e\x08\xa7\x08\x4a\xe9\xbf\x6d\xa7\x96\x32\xcc\xe6\x5c\x18\x5d\x7e\xff\x38\x51\xdb\xe0\x08\x86\xf4\x39\x00\x8b\xc8\x35\xb2\x9c\x76\xc3\x2d\x92\x2f\x65\x65\xea\x63\x46\xdc\xf9\x39\x5e\xc5\x7e\x60\xea\x2a\xf4\x0f\x4c\x24\x81\xe1\x52\x91\xac\x69\x53\x3f\x50\x3b\x7d\x95\x1e\xf5\xce\xda\xe1\xba\x73\x40\x24\xe3\xbb\x07\x42\x54\x9a\xec\x0e\xd9\x5a\x90\x2a\xf6\xa2\x70\xde\xfb\xef\x9d\xf1\x12\xe9\xae\xaa\xc6\xb0\x85\xb5\xab\x47\x6a\x0f\x76\xa3\xe5\xb8\x68\x0b\xae\x6c\xe6\xbf\x09\x9c\xfb\xc8\xeb\x72\xab\xc5\x7a\x44\x34\xc1\xe3\x36\x42\xcb\x2c\x5b\x38\x7a\x86\x83\x13\xfd\xda\x2d\x92\xfa\x4d\xbf\x49\x1b\xa6\xfa\xe3\x62\xdd\x10\x5e\x5e\xc5\xcb\x18\x4e\x8b\x8e\xba\x93\x81\xd9\x2c\xbb\xb6\x7e\xf3\x7a\x48\xe0\x49\xcb\x83\x1e\x97\x82\x6d\x5a\x82\x87\x8c\x49\xac\x6c\x42\x7a\xeb\xa7\x8d\xd8\x9f\xeb\x0c\x2b\x62\xf7\x52\x2e\x75\xdf\x89\xb6\xcd\xe0\xc1\x69\x4e\x98\xe3\x21\x6e\x92\xe0\xee\x94\x4b\x2a\xfe\xd0\x93\x06\xec\x92\x3b\x7e\x5b\xfd\xa8\x36\x3c\xfa\x39\xa8\xbd\xae\x7e\x49\xde\xfd\xe3\x15\xdc\x56\x6a\xd4\x2b\x59\x38\x04\xdb\x71\xb5\x72\x8b\x19\x8a\x83\xc5\x0d\xeb\x1f\xa2\xc0\x1d\x15\x32\xc9\xf7\xbd\xfd\xb2\xd9\xdd\x4c\x81\xcc\x8a\xb9\x0b\x23\xd6\xd0\x10\x36\xb9\xca\x75\x51\xba\x20\xc5\xa3\xb5\x6b\x2c\x21\x8b\x36\x22\xe2\x78\x88\x79\xd2\xde\x4a\x29\xd5\x53\x1a\xe1\x4b\xe5\x7a\x3d\x03\x72\xdf\x3f\x28\x91\xea\xf3\x1a\x13\x69\xad\x69\x75\x9c\x06\xe9\x41\xb1\x03\xa3\x91\x74\x39\x0b\x66\x48\xa4\xbc\x35\x62\xd0\x50\xa9\xea\x2d\xa5\x1d\xe5\xb5\xa6\x47\xc0\xf5\xa5\x06\xc8\x35\x05\x2a\xb6\x37\xe9\x54\xb7\xa9\x5a\xc5\x9c\x5d\xfb\x72\x02\xdb\x74\x0a\x50\xca\xde\x9b\x4e\xac\xc1\x3b\x58\xf5\xf4\x32\xb0\x67\xd5\x47\x5b\xb4\xfe\x6a\xf3\x8c\xb8\xb2\x31\x84\x95\x1d\x47\x44\xc7\x9f\x2e\x15\xe5\x68\xdc\xad\x68\xce\xaf\x7d\x99\xcb\x0b\x7c\xa6\xd3\x64\x78\x35\x03\xf3\x45\xce\x9b\xf2\xf6\x0e\x84\xb8\xda\xf7\xdd\x3b\x03\x60\x8f\x7e\x6f\xcc\x52\xcd\xe8\xd4\xb9\xab\x76\xb9\xa5\x9c\xfe\x32\x86\x31\xbc\x96\xe9\xed\x5a\x9c\xce\xce\x19\xdf\xc3\x37\x7e\xeb\xd4\x02\x90\xfb\x3b\x81\x87\x9e\x28\xaf\xd3\x57\xb8\x5a\x75\x66\xc3\x20\xaf\x13\x7f\xb7\x36\x36\x86\x4c\xbc\xd8\x2f\xea\xec\x6a\xf5\x82\xfe\x63\x47\x87\x9f\x4a\x7b\xee\x72\xcc\x15\xbd\xdc\xf5\x1b\xae\x36\x26\x34\xc0\xed\xe8\x96\x4f\x66\x30\xab\x64\xb7\xd8\x26\x25\x36\x5f\xde\xf6\xa9\x9c\x97\x7a\x33\x4b\xc7\x9b\x52\x5e\x71\xbb\x63\xe7\x96\xf5\x94\xc7\x95\x19\xdb\xae\xaf\x47\x8f\x6e\xb6\x8e\xcc\xde\xba\x33\x75\x7c\xaf\xfa\xe1\x98\xde\x13\x89\x63\x95\xb2\x8f\x18\xa1\x95\xfe\x83\x54\x2e\x8c\x0b\x61\x32\xd3\x58\xc1\x47\x81\xf2\xe2\x50\x24\x86\x83\x5b\x66\xbb\xc5\x2f\xf7\x0d\xd7\x41\x01\xe1\xcb\xb7\x67\x92\x64\x6b\xee\x65\x6d\x3b\x76\xd5\xce\xea\xd8\xb0\xf3\x83\xf5\xa8\x2b\x52\xa5\x3c\x88\x47\x50\x4c\xc9\x65\x76\xc1\xc1\x9d\xef\xe9\x85\xf5\x55\x89\xc9\x2d\x1c\x00\x78\x4a\xf9\x77\x3b\x00\xcb\xd5\xbd\x0e\x74\x05\x00\xbe\x3f\xdf\x6f\xa0\xfa\xf6\xee\x2e\x56\xae\x96\x0e\xce\x70\x01\xc4\x9f\x1f\xf9\xa1\xae\x76\x51\xba\xda\x6a\xa4\x44\x0c\xdf\xef\x3d\xa4\xea\xf7\x94\x1f\x02\x00\x50\x0b\x00\x58\x00\x01\x36\x00\x00\xed\xe7\xdd\xab\xdf\xb1\xba\x9a\xfa\x2a\x62\x62\x62\xd2\xd2\xd2\x0a\x0a\x0a\x9a\x9a\x9a\x7a\x7a\x7a\x20\x10\xc8\xca\xca\xca\xd9\xd9\x19\x06\x83\xf9\xf9\xf9\x85\x84\x84\x44\x47\x47\x27\x25\x25\x65\x66\x66\xe6\xe7\xe7\x97\x96\x96\x56\x57\x57\xbf\x79\xf3\xa6\xbd\xbd\xbd\xb7\xb7\xf7\xe3\xc7\x8f\x53\x53\x53\x5f\xbf\x7e\x5d\x5d\x5d\xdd\xdb\xdb\xbb\xb8\xb8\x00\x00\xe0\xf2\xf2\xd2\xc5\x2b\xf1\x0c\x00\xb0\xda\xd4\x95\x15\xf4\xbd\x26\x31\xa3\xd3\xc8\xe7\x82\x14\x78\x1f\x6c\xa2\xb0\x42\x02\xee\x71\x69\x9c\x5d\x08\x80\x12\xef\xa7\xd4\x5d\x6b\x7e\x35\xb1\x16\x83\x53\xf7\x65\xc9\x5e\x34\x63\x0b\xdd\x62\x6d\x5a\xfc\xf2\xaf\xb8\x2c\xe6\x88\x5d\xf5\xce\xec\x4e\x5a\x30\xf1\x0f\x10\x58\xf5\xc2\xd8\x19\x40\x9c\xea\xdf\x71\x3c\x2b\xaf\xe3\x5b\x3a\xd5\x47\xa5\xed\x26\xa2\x28\xd7\x6b\xcc\x98\x87\x17\x32\xa9\xaa\x3c\x8e\xc5\x3d\x5e\x4f\x81\xa2\xbd\x5b\x48\xc7\xd3\x04\xc0\x6b\x64\x67\x6a\xe7\x91\x96\x72\x45\x8d\x7e\xb7\xe7\x5e\x28\x9a\x1b\xaf\x2f\xa0\xc7\xd7\x25\xcc\xef\xaf\xa0\xa1\x98\x5e\x77\xe6\x31\x78\x49\xf6\x90\x35\xea\xe8\xc8\xa2\xb5\x7c\xab\x77\xe1\xf4\x70\xbd\xbb\xfe\x13\x5b\xf6\xf3\x01\x35\xad\xf3\xad\x51\x77\xf3\x8b\x0c\x76\xf7\x80\x4c\x9d\x66\x96\xb6\x34\xbd\x5e\x87\x32\x8e\x72\xb1\x94\x7c\x98\x13\xb7\x4b\x03\x33\x5b\xc3\xc8\x9c\x64\xa6\x4e\xf3\xa0\x03\x7d\x3b\x05\x5a\xe6\x70\x21\x2e\xb1\xe9\x58\x5c\xa0\x6c\xfa\x84\xf1\x84\x9b\xcc\x14\x69\x2c\x91\x7d\xe2\xbd\x7f\x52\xf8\xc2\x91\x60\x41\x22\x75\xea\xa6\x8e\x9a\x66\xb8\x3f\xe3\x89\x24\xd9\xdc\xca\x3e\xa1\xef\xb4\x19\xe1\xc4\xe4\xb9\x06\x81\xff\xa6\x79\xd5\x30\x59\x2e\xea\x7f\x6f\x5c\xcc\x8e\xdc\xc5\xd2\x92\xca\xbb\xbd\x05\x51\x79\xf9\xbd\x98\xa9\xab\x68\x2b\x97\x2b\x5a\x04\xff\xbe\xdc\xd1\x00\x00\xa0\xa5\xa2\xaf\xc0\xa7\xae\xad\x2a\xf0\xff\xcc\x0a\x2f\x17\xe7\xb2\x24\x05\xd7\x0e\x56\x0a\x95\x23\xc9\xb0\xae\x3a\xe1\x9a\xc7\x06\x5c\xda\xb4\x09\x3e\xc1\xfb\xc0\x08\x33\x23\x3b\x1d\x05\xc9\x03\xd8\xa9\x15\xaf\xb6\x89\x41\x82\x15\xb7\xb4\xc5\xbb\xad\xcb\xe3\xb4\x81\xcd\x0d\x67\xf7\xe0\xb4\x98\xee\xf0\x27\xdb\x9e\x8f\xb0\xea\x5a\x44\x26\xee\x6c\x9c\x6e\x1b\x04\x54\x4d\x7b\xd4\xba\x11\xc5\x86\x2b\xc3\x40\xfa\x8b\xd2\x9a\xf5\x2e\x1d\x30\xe5\x55\x03\x61\xf5\x3c\x58\x3c\x7b\x2d\x1a\xff\xed\x60\xfd\x27\xac\xc4\xb3\x29\x01\xb5\xc2\x29\x57\x5b\x8b\x81\x72\xfb\xf5\x39\x19\x37\x6b\x01\xce\xf8\x43\x7a\x69\x95\xc2\x53\x1e\x9c\x65\xe0\x23\x09\x07\xb8\x70\x7c\xb2\x86\xd5\x2e\xf4\x81\x43\x53\xc9\xe6\x4d\x7b\xeb\x5c\x7e\xcc\x0e\xec\x4e\x4e\x75\xad\xbb\x2e\xc2\x39\xcd\x6b\x65\x9d\xf9\x0a\x42\x75\x14\x41\xf8\x41\x90\x26\x58\x0c\x63\x7f\x2f\xb3\x72\xc1\x1a\xcf\x4f\x4e\xaa\xc8\x2a\x24\x8a\x83\x99\x31\x7e\x1e\x1c\x89\x9f\x81\xad\x4b\xb2\xa8\xf2\x01\x47\x79\x11\x34\xdc\xd7\xe6\x1b\x37\xb9\x2f\x89\x64\x2b\xa8\xf5\x12\x7e\x99\xec\x56\xd9\x5e\x3d\x87\xc9\xac\x14\x2c\x59\xa7\x8f\xcb\xf2\x57\xfc\x03\xd1\xcc\xa7\x83\xe4\xa7\x35\xcb\x36\x4e\xcf\x08\xbf\x5c\x18\xfd\xdc\x21\xd4\xc4\xa2\xff\xee\x96\xc2\x70\x22\xfe\x06\x4b\x24\x5d\x91\x0f\x85\x52\x73\xb1\x73\x67\x6b\x51\xcb\x39\xee\xf7\x04\x70\x7e\x23\xf8\x39\x3f\x16\x00\x70\xe0\x7c\x4f\x00\x58\x57\xa8\x81\xdf\x77\x4f\x7f\x8e\x3f\x7b\xa9\x7f\xa2\xfe\xdc\xa8\x1f\xdb\x65\x3f\xa3\xb4\x7e\x68\x9a\xfe\x1d\xf5\xa3\x2f\x49\xf2\x13\xca\xfc\xca\xcf\x8d\xb5\xbf\xcf\xf2\xef\x26\xdb\x5f\x71\x9b\xf0\x7f\x72\x31\x7f\xcf\xc4\xf0\x13\x93\xed\x2f\x98\x7e\x74\x35\xff\x2d\xcf\x8b\x5f\xf0\xfc\xe8\x72\xfe\x9e\x87\xe5\x27\x9e\xb1\x5f\xf0\xfc\x7f\xae\xe7\xef\xc9\xe8\x7e\x22\xa3\x22\xfa\x27\x17\xf4\xdf\xb2\xa8\xff\x82\xe5\x2f\x57\xf4\xdf\x6e\x59\xc0\x2f\x58\x7e\x76\x49\xff\xed\x7c\x1a\x7e\xc1\xf4\x97\x6b\xfa\x6f\x37\x6c\xeb\x37\x2c\x7f\xba\xa8\x3f\x1f\xe2\x1f\x2d\xc2\x9f\x0f\x31\x27\xf1\xcf\xae\xea\xdf\x91\x3f\xca\x7c\xa2\x9f\x90\xb6\x24\x3f\x1a\x8a\x7f\xc7\xfd\x58\xbf\x89\x7f\xc2\x7d\xa6\xfe\xe9\x4a\xf0\xf7\xbf\xfc\xf7\xf2\xfe\x57\x48\xd3\xff\xbe\xd8\xff\x7d\xf8\x1f\xb3\x07\xcd\x4f\x2c\x7d\xd7\x7f\x93\xa2\x75\x35\x70\xf1\xbe\xff\xe0\x2a\x70\x15\x00\xe3\x00\x80\x0c\xd3\xf7\x6f\xff\x27\x00\x00\xff\xff\xae\xb0\x15\x1b\xb7\x21\x00\x00")
+
+func testAssetsTest_templateOdtBytes() ([]byte, error) {
+ return bindataRead(
+ _testAssetsTest_templateOdt,
+ "test/assets/test_template.odt",
+ )
+}
+
+func testAssetsTest_templateOdt() (*asset, error) {
+ bytes, err := testAssetsTest_templateOdtBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "test/assets/test_template.odt", size: 8631, mode: os.FileMode(420), modTime: time.Unix(1568821589, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+// Asset loads and returns the asset for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func Asset(name string) ([]byte, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
+ }
+ return a.bytes, nil
+ }
+ return nil, fmt.Errorf("Asset %s not found", name)
+}
+
+// MustAsset is like Asset but panics when Asset would return an error.
+// It simplifies safe initialization of global variables.
+func MustAsset(name string) []byte {
+ a, err := Asset(name)
+ if err != nil {
+ panic("asset: Asset(" + name + "): " + err.Error())
+ }
+
+ return a
+}
+
+// AssetInfo loads and returns the asset info for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func AssetInfo(name string) (os.FileInfo, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
+ }
+ return a.info, nil
+ }
+ return nil, fmt.Errorf("AssetInfo %s not found", name)
+}
+
+// AssetNames returns the names of the assets.
+func AssetNames() []string {
+ names := make([]string, 0, len(_bindata))
+ for name := range _bindata {
+ names = append(names, name)
+ }
+ return names
+}
+
+// _bindata is a table, holding each asset generator, mapped to its name.
+var _bindata = map[string]func() (*asset, error){
+ "test/assets/test_expected.pdf": testAssetsTest_expectedPdf,
+ "test/assets/test_template.odt": testAssetsTest_templateOdt,
+}
+
+// AssetDir returns the file names below a certain
+// directory embedded in the file by go-bindata.
+// For example if you run go-bindata on data/... and data contains the
+// following hierarchy:
+// data/
+// foo.txt
+// img/
+// a.png
+// b.png
+// then AssetDir("data") would return []string{"foo.txt", "img"}
+// AssetDir("data/img") would return []string{"a.png", "b.png"}
+// AssetDir("foo.txt") and AssetDir("notexist") would return an error
+// AssetDir("") will return []string{"data"}.
+func AssetDir(name string) ([]string, error) {
+ node := _bintree
+ if len(name) != 0 {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ pathList := strings.Split(cannonicalName, "/")
+ for _, p := range pathList {
+ node = node.Children[p]
+ if node == nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ }
+ }
+ if node.Func != nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ rv := make([]string, 0, len(node.Children))
+ for childName := range node.Children {
+ rv = append(rv, childName)
+ }
+ return rv, nil
+}
+
+type bintree struct {
+ Func func() (*asset, error)
+ Children map[string]*bintree
+}
+var _bintree = &bintree{nil, map[string]*bintree{
+ "test": &bintree{nil, map[string]*bintree{
+ "assets": &bintree{nil, map[string]*bintree{
+ "test_expected.pdf": &bintree{testAssetsTest_expectedPdf, map[string]*bintree{}},
+ "test_template.odt": &bintree{testAssetsTest_templateOdt, map[string]*bintree{}},
+ }},
+ }},
+}}
+
+// RestoreAsset restores an asset under the given directory
+func RestoreAsset(dir, name string) error {
+ data, err := Asset(name)
+ if err != nil {
+ return err
+ }
+ info, err := AssetInfo(name)
+ if err != nil {
+ return err
+ }
+ err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
+ if err != nil {
+ return err
+ }
+ err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// RestoreAssets restores an asset under the given directory recursively
+func RestoreAssets(dir, name string) error {
+ children, err := AssetDir(name)
+ // File
+ if err != nil {
+ return RestoreAsset(dir, name)
+ }
+ // Dir
+ for _, child := range children {
+ err = RestoreAssets(dir, filepath.Join(name, child))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func _filePath(dir, name string) string {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
+}
+
diff --git a/test/bindata.go-e b/test/bindata.go-e
new file mode 100644
index 000000000..bbe0896d7
--- /dev/null
+++ b/test/bindata.go-e
@@ -0,0 +1,262 @@
+// Code generated by go-bindata.
+// sources:
+// test/assets/test_expected.pdf
+// test/assets/test_template.odt
+// DO NOT EDIT!
+
+package test
+
+import (
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func bindataRead(data []byte, name string) ([]byte, error) {
+ gz, err := gzip.NewReader(bytes.NewBuffer(data))
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+
+ var buf bytes.Buffer
+ _, err = io.Copy(&buf, gz)
+ clErr := gz.Close()
+
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+ if clErr != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+type asset struct {
+ bytes []byte
+ info os.FileInfo
+}
+
+type bindataFileInfo struct {
+ name string
+ size int64
+ mode os.FileMode
+ modTime time.Time
+}
+
+func (fi bindataFileInfo) Name() string {
+ return fi.name
+}
+func (fi bindataFileInfo) Size() int64 {
+ return fi.size
+}
+func (fi bindataFileInfo) Mode() os.FileMode {
+ return fi.mode
+}
+func (fi bindataFileInfo) ModTime() time.Time {
+ return fi.modTime
+}
+func (fi bindataFileInfo) IsDir() bool {
+ return false
+}
+func (fi bindataFileInfo) Sys() interface{} {
+ return nil
+}
+
+var _testAssetsTest_expectedPdf = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x79\x09\x34\x94\x7d\xff\x77\x91\xa5\x11\xca\x96\xdd\xc8\x92\x7d\xf6\xc5\x5a\xf6\x08\xd9\xc9\x3e\x18\x8c\x65\x86\x31\x76\x12\xb2\x93\x10\x65\x8f\xc8\x16\xd9\x25\x6b\x54\xf6\x5d\x59\xb3\xdf\xb2\x86\xc8\x1a\xbd\x47\xdd\xef\xf3\x3c\xff\xbb\xe7\x7d\xff\xd7\x39\xd7\x75\xae\xef\xe7\xfb\xb9\x7e\xdf\xed\x33\x67\xce\xf9\xfd\x04\xb4\x95\x54\xc4\x21\x12\x70\x80\x40\x5b\x6e\x5b\x43\x5b\x4d\x5b\x16\x00\x0a\x04\x03\x09\xd6\x8e\x00\x19\x19\x90\x06\x16\x6f\x4f\x72\x00\xc2\x80\x60\xa0\x2e\x48\x05\xe7\x4c\xc2\x12\x41\x2a\xce\x18\x12\x56\x09\x6b\x43\xb0\xc5\xca\xc9\x01\xdc\x49\x44\x2c\xc6\x05\xe0\x9d\xee\xd2\x23\x4d\xd3\x7a\xf3\xf2\x85\xd9\xd3\x7b\xef\x3f\x29\xf4\x8d\xfb\xe1\x67\xd1\x60\x39\x40\x7d\x4b\x50\xe8\x18\xdb\xc6\x85\xe1\x83\x7d\x73\x6d\x0d\xbe\xe1\x5e\x30\x8f\xd8\x70\xb7\xcc\xc5\xd4\x31\xd5\xb5\x5b\x64\xa6\xd9\x3e\xd0\x7c\x66\xd9\x26\x1a\x87\xa6\xb4\xec\x94\xa7\xa4\x2e\xb5\x97\xf9\x9e\x79\xdf\x83\x22\x43\x27\x36\xcc\x5a\x0c\x72\x5b\xde\xf2\x01\x0d\xbf\x24\x0b\xf2\x2a\x0e\x38\x6d\xae\xe6\x71\xf5\x36\xa5\x6c\x83\xd6\x2a\x63\x6f\x20\xfd\xc7\x62\xb9\xf3\x3c\xcd\xc0\x42\xd5\x25\x19\xad\xf5\xa1\x6c\x2b\xb4\x96\x11\xc2\xa2\x00\x2c\xde\xf6\xef\x8c\xb0\x78\xdb\xb3\x22\x00\xb0\xbf\xab\x81\xc0\x10\xff\xc2\x10\x7f\x54\x88\xfc\x7f\x54\xf8\xb7\x1f\x02\x94\x44\xc3\xc1\xff\x59\xee\x5f\x68\x1f\x12\xc7\xf7\x94\x19\x14\xdf\xdd\x0a\xff\x5b\x44\x31\x1e\x9d\x60\x95\x94\x31\xad\x79\x9b\x74\x21\x2f\x85\x97\x1d\xc9\x71\x63\x6e\x9c\x3c\xf2\x44\x85\xf5\x48\x01\x53\x40\x4d\xae\x7a\x94\x22\x99\xf3\x54\xb2\x2b\xfa\x82\x02\x4e\x78\x63\xef\x59\xbd\x73\x9a\xaa\x4f\x27\xeb\x98\xe0\xd6\x5e\xbc\xeb\xe3\x80\x7a\xd3\xe9\x3d\xf1\xb2\x3c\x71\xdc\xb0\x84\x8b\x17\x8d\x58\x15\x05\xb7\xb8\xd2\xf4\xe6\xfe\x70\x87\x9b\xc2\xc4\xd7\xb5\xc9\x9f\x9e\xb9\x29\x87\x5f\x8f\xf7\x87\x1d\x86\x3a\xc9\x31\x42\x0f\x84\x2f\xbf\x95\x86\x66\x0b\x3e\xee\xaa\x60\x38\x77\xc0\x7d\x2e\x39\x46\xe3\x45\x5e\xff\xde\x54\x43\xca\x6c\xf4\xb9\x56\xa7\xe7\x91\xe5\x91\xa0\xac\xf1\x1d\xee\xfb\x73\x62\xf7\x57\x3a\xcb\xb7\x15\x9f\xd7\x37\x37\xad\x52\x9d\x0b\x5d\x38\x97\xfc\x3d\xbe\x8c\xfd\xa8\x3d\xf1\x95\x21\xf9\x63\x59\x05\xe4\x27\x95\x88\x81\x2c\x49\xdb\x6b\x3f\x3c\x5a\x9f\xa8\x08\x0e\xa1\x5e\xa1\xd6\xbf\x9d\x7b\xcc\x08\x58\x6b\x5f\xe0\x5b\x65\x98\x6c\xf3\x6c\x1d\x65\xbc\xf7\xf9\x81\xaf\xfb\xfc\xa3\x2d\xa7\xa0\xad\x73\x73\xba\x1a\x19\xd4\x33\xa3\x7e\xbb\x0c\x73\x28\x72\x26\xce\xdb\xef\xf4\x56\xbe\x60\x56\xc4\xee\x3b\x5b\xfd\x50\x82\xa6\xb3\x7e\x3e\xe8\x35\xa2\xb6\x04\x00\x3d\x2f\xd3\xae\xb4\x3d\x54\x20\xdb\x61\x95\x88\xa0\xb4\xc9\xec\x4c\x27\x08\x0b\x89\x05\xff\x0c\x0f\xdf\x46\xa3\x81\xf8\x06\xee\x97\x47\xdf\xa3\x14\xfd\x2f\x89\xcc\xb9\xdd\x1f\xaf\x65\xf5\x3c\x4c\x5d\x49\x3d\x69\xfb\x89\x65\xb2\xbb\xf7\x9e\x81\xd4\xe6\x35\xb7\xbf\x2c\x7f\x1e\xff\x9e\x02\x4c\xcd\x1b\xd1\x56\x02\x8e\xcd\x3c\x1c\x7a\x7d\xb1\x3e\xc2\x8b\xa3\x41\xeb\x07\x2b\xa7\x2a\xa3\xff\x86\x89\xe1\x60\xec\x47\x63\x25\x16\xca\xba\x29\x97\x2d\x86\x1f\x3f\x52\xbf\xde\xa0\xe8\x6f\xa9\x37\x3a\x02\x4e\x84\x6c\x51\xcb\x7c\x58\x1e\x53\xaf\x91\x8b\xeb\xb7\xb9\x1a\xd6\x41\x59\x39\xa5\xfc\x44\x76\x14\x2a\xb0\x9e\xdf\x1d\x40\x9a\xd6\x18\xbd\x78\xde\x63\x55\xba\xca\xda\x22\x44\xb6\x15\xd6\xca\xe1\x4a\xa6\xb5\xb2\xcd\xdc\x66\x3a\xa6\xb0\xaf\x15\xb5\x15\x83\x7f\xc6\x78\x24\x87\xd9\x15\x53\xc6\x04\x41\xd0\x17\xbc\x1e\x21\x6f\xc4\x8c\x06\xfe\xe0\xeb\xc8\x3a\xe1\x1a\x9f\xa0\xec\x67\xc8\x26\xc7\x07\x2d\xb8\xb3\xf3\x5c\xdb\xe7\x35\xb1\xf2\x7c\xe8\xa9\x48\x07\xae\x8e\x3e\x0e\xdd\xa4\x6e\xcd\xa3\xa2\xae\xcb\x5a\x9f\xa6\x17\x39\x7d\x94\x23\x23\x81\x6f\xb4\x0b\x38\x0d\x56\xe4\xbc\x7f\xcd\x4c\xc2\x50\xfe\xb4\xfa\x75\xab\x63\x99\xdd\x29\xf0\x65\x6a\x80\xaa\x19\x80\x85\xf5\x69\x00\x4a\x35\x6d\x23\xfd\x24\xa3\xe0\x74\xd3\x7f\xf1\x33\xe1\x29\xd0\xe5\xe9\x89\xf2\x52\xb2\x46\x47\x01\x2b\xcd\x60\x24\x62\x64\xdb\xb2\x2d\xae\xd1\xa5\x9e\x2a\x24\x28\xe8\xb4\x69\x3a\x43\x56\xfa\x4d\xad\x97\x07\x7e\xcd\x81\xe0\xf4\xbd\x16\x7e\x5c\xe3\x9b\x56\x63\xd6\xec\x33\xfc\x81\x76\x6d\xb4\xc4\xbc\xb2\x42\x07\x57\xf0\xca\xd4\x50\xcf\xbc\x5a\xdf\x62\xde\xd3\xe0\xf1\x84\xcb\xa7\xf9\x1c\xb2\xdb\x4d\xe3\x8f\xed\xe1\x11\x23\xcd\x52\x6a\x6d\x98\x66\x41\x43\xb5\x3d\x43\x83\xbd\x6e\x47\xbf\x80\x8c\xc5\x57\x11\x55\x23\xa3\x87\x7f\x7d\xc2\x68\xea\x0a\xc4\xe2\xc3\x1c\xeb\x1a\x33\x12\x6d\x0d\x12\xc3\x04\x4a\x52\xf1\xab\x32\xdf\xca\x96\x04\x3a\x07\x8b\xf9\x1b\xef\xaa\x6d\xa6\xf3\x4f\xe4\xf6\x96\xa5\xd4\x3d\x12\xd8\xc8\x63\xb9\x2b\x15\x1d\x22\x4d\x7e\x81\x87\x10\x1f\x92\xb0\x87\x0c\xaf\x75\x1f\xd3\xee\x8d\xbf\xa6\x30\x85\x1f\x9a\xd5\x29\x77\x10\x68\xfe\x24\xc5\x96\xb2\x47\xaf\x17\x83\xd6\xb3\xc2\x25\xa7\xb5\x25\x1f\x49\x3d\x2d\x67\xba\x6c\x7c\xf3\x51\x8d\x86\x56\xcf\x6c\x85\x78\xc7\x79\x4c\x4a\xe0\x42\xcc\xb9\xa5\x6b\x4b\xc9\xd0\xef\xc5\xbc\x25\xf6\xf6\x2d\x33\xf3\xa4\xfa\x90\xef\x1e\x5c\x26\xe5\x53\xad\x77\x07\x7a\x34\x1d\x7d\x7a\x53\x23\xd3\x1e\xe7\x88\x91\xd0\x82\xbd\x23\x19\x59\x4d\x5f\x5e\xd2\xee\xd1\x8e\xb2\x84\x2d\x3e\x7a\x73\xfd\x59\xa4\xc8\x8a\x56\xf4\xe9\xca\x8a\x34\xc2\xaf\xa4\xc6\x0c\x3f\x98\xcc\x73\xef\x79\x30\x94\xde\xc9\x83\x18\x6d\x59\x8c\x35\x94\xf6\xfb\xf2\x83\x64\xe1\x19\x99\x5a\xde\x25\x78\x7b\x78\x93\x3c\x8f\xaf\xf9\xe4\x53\x12\x93\x41\xfe\xa2\xc1\xf8\x34\x15\xa5\xa3\x42\x65\x58\xe3\x79\x94\x74\x7a\x89\xfe\xda\xca\x8a\xe1\x5d\xc7\x0c\xee\x4c\xc3\xfe\x3e\xad\x8d\x44\x81\x34\x19\x59\x3d\x63\x74\xc6\x96\x5e\x5d\xf6\xf5\x9b\x82\x7d\x7d\x72\x01\x3d\x85\xd5\xd3\x8c\x23\xce\x6d\x57\xa6\x58\x47\xf4\x08\x1b\x1b\x23\x2c\xeb\x53\x5c\x03\x0f\x8a\x0c\x85\x73\x18\x42\x9b\x34\x2b\xfc\x1a\x87\x5d\xb2\x12\x57\x0e\x6d\x9c\x9f\x55\x68\xbb\x15\xb4\xe2\x51\xa2\x6d\xe5\x95\x16\x63\xaa\x11\xc8\x6a\xd8\xa3\xf7\x02\x63\x5f\xfc\x39\x1b\xd4\x5d\xb4\x19\xc3\xd8\xbe\xf8\x3f\x5c\x60\xb5\x4a\x2f\xe4\xb4\xd7\x0a\x30\x7e\x31\xed\xb9\x47\xf1\x02\xa1\x68\xa0\x7a\x6b\x4f\xef\x7d\x5c\xe1\xc4\x88\x5c\x6a\xe9\xc9\x4b\x48\x85\xa6\xe8\x69\xcb\x4a\x72\xfe\xed\xf5\x21\x4a\x81\x0e\xac\x40\x54\x11\x1f\x53\x12\xdc\x06\xc1\x1c\x59\xb5\x24\x3a\x30\x05\x4e\xc2\x71\xa8\x46\xdd\xe8\x98\x9e\xf6\x4c\x08\x71\xf4\x8b\xac\x92\x17\x84\xf5\x3c\xb4\xdc\xa3\x5f\x9b\xe9\xbc\x45\xc1\xf6\xa8\x60\x60\xa3\x7a\x5c\x7e\x99\xde\x61\x9a\x85\xac\xa1\x68\x1a\xf2\x53\xe3\xb9\x02\xfb\x2a\x18\x21\x36\xd1\x9a\x56\x28\x4c\x54\x10\x4d\x0a\x70\xff\x8c\xc5\x1c\x3e\x0f\x38\x5f\x9a\x53\x8c\xcb\xeb\x65\xbf\x14\xb7\x6c\x92\x3a\xff\x65\x4d\xfd\xdb\x1d\xfa\x5a\x5b\x45\xab\x9a\xc5\x8c\xa0\xfc\x16\x98\x9f\x9b\x5d\xf5\xa2\x7f\x2b\x79\x31\xef\x58\xeb\x83\xee\x54\x5c\x57\xd1\xc9\x70\xd0\x40\xc1\x12\xbb\x67\x98\xc3\xe7\x58\xf5\xf3\xf9\xa6\x3b\x94\x85\x6d\xf2\x92\x97\xe9\x2a\xca\x9f\x03\xc4\x9b\x97\x17\xdb\xae\x2c\x56\x0e\xf1\x2c\xaf\x27\x4a\x7b\xd1\x51\x7e\x5a\x0b\xb2\x65\xeb\x49\xcf\xee\x0a\x4c\x62\x93\x9c\xed\x49\xcd\x3e\x3f\xd2\x63\x60\x27\x0c\x44\xbf\x07\x5e\x9c\x88\x00\x56\x33\x00\x8f\x91\xc3\x06\x30\x83\x94\x95\x2a\x9d\xb7\x92\x49\xb4\xa5\x84\x4a\xa6\x9b\xd7\x17\x73\x85\x2f\xb9\xd4\x2d\xd2\xd7\x2e\x13\x2f\x35\xb7\x16\xc4\xc7\xea\xb9\xfb\xf5\x99\x70\x2b\x76\x6a\xb9\xd7\x0f\x73\x91\xec\xe3\x8e\xa4\x82\xc6\x12\xbe\x3e\x85\xf7\xbc\x2c\xd8\x90\x64\x71\xd1\x7b\x07\x1e\xf6\x47\xd6\xba\x33\x14\x63\x29\x38\x7c\x85\x81\xe1\x7f\x3d\xe3\x8c\x87\x13\xee\xdc\xff\xeb\x43\x57\x5b\xf1\xcb\xab\x21\xa3\x66\x9b\x06\x3f\x99\xc7\x1a\xac\x75\x52\x5a\x34\xd7\x39\x5b\xa3\x0a\xab\x3f\x49\x7d\xb9\x6a\xe4\xb4\x70\xc0\x39\x71\x8f\xb9\x9e\xaf\x3d\xf9\x45\x2f\xd1\x7e\x06\x0e\x93\x58\xa3\x0c\xba\xb6\x39\x70\x3f\xb6\xcc\xab\xf1\x73\xe7\x3e\x59\xcd\x33\xec\x84\x9f\x00\x87\x38\xae\xe9\x59\xbe\x7d\x83\x45\xcd\x53\x3e\x8e\x94\x2b\x75\x7c\x70\xf1\x7d\x0a\xa4\x3b\x6e\xe2\x0e\xde\x9d\xdb\xae\x49\xfd\x19\xa3\xf4\x71\xc0\x77\x8f\xaa\xea\x43\x3b\xdb\x06\xa7\x2f\x31\x57\x37\x57\x39\x2e\xc9\x60\x9f\xdb\xb7\xd5\x1f\x91\xc7\xd1\x08\xc1\xe9\x8f\x83\x61\xb7\x23\xaf\x7a\xab\x93\x6d\x11\x8b\x5a\x0a\x62\x3a\x2e\x47\xf2\xf6\xe8\x82\x78\xed\x24\x36\xfd\xbb\x68\xc0\xef\xeb\x28\xb5\x66\x0f\x3a\xee\x5e\xd0\x82\x7d\x71\xa0\xe6\xb3\x0f\x15\xeb\x4d\xf6\xd0\xef\xe5\xf2\x91\x07\x3e\xe4\xed\xde\x5e\xca\xa5\x55\x82\xd9\xe3\xf4\xd2\x3f\xc4\xb1\x59\x09\xec\x98\xc4\x58\x54\xa2\x66\x4c\x85\x98\x2e\xf6\xb1\xd9\xe6\xb6\x47\x7e\x83\xcb\x9a\x2f\xf2\xc9\xe8\x44\x28\x2f\xa7\xf3\x96\x58\x68\xc9\x45\xeb\xc1\x0e\x2d\xec\x0d\xad\xa3\xf8\x1b\xc6\xa9\x37\xd9\x5f\xaf\xfb\x5a\x81\xb7\x94\x78\x4f\x14\xa4\x0f\x93\x38\x0f\xc7\xe4\x17\x74\xaf\x64\xdb\x0d\xf7\x47\x59\x02\xca\xe5\xeb\x10\xac\x29\xdc\x74\x15\xef\x29\xb1\x23\xec\x47\x39\x61\xdf\x58\x1c\x13\x1b\x42\xc9\x3a\x45\x47\xc4\x88\x63\xf9\xcc\x1d\x72\x2a\x04\xeb\xdd\x17\x62\xb2\x71\x83\x9a\xf0\x1c\xe3\xb0\x5c\x39\xe5\xc6\xcd\xc1\x47\x9a\x2d\x5f\xef\xa7\x25\xe7\x56\x6d\x65\xa5\x0f\xd7\xd4\xde\xcd\x0f\xae\x2a\xd4\x98\xbc\xa7\x73\x87\xf9\xb9\x20\xeb\xe8\xa0\xfb\x32\xd9\xd3\xad\x23\x00\x0d\x60\x5d\xb0\x72\x5c\x82\x3a\x8f\x2e\xed\x29\x88\x8e\x8a\xa6\xf7\x30\xc7\xfc\x1c\x46\x88\x5a\xc0\x82\x0a\x29\x35\xb0\x74\xd3\x75\xb6\x9e\x4e\x66\xb3\x85\x91\x2e\x9d\x68\xd5\x1a\x24\x4b\xc7\x5f\xba\xd8\xc2\xf1\x7d\x41\x8f\x74\x53\xaf\x84\x4b\x86\x59\x93\x9b\xbe\x7e\xf2\xb6\x6b\xc8\xe7\x85\xf2\x9d\xfe\x2e\x95\xfd\x54\x0f\x4f\x9f\x48\x66\x6c\xfd\xa4\xc4\xa8\xfe\x68\x4e\xe3\x54\x90\x93\xe5\xf7\x1f\xf3\x2a\x5f\xf0\x36\x87\xdc\xf6\x33\xc7\xa2\x96\x1b\xae\xec\xf3\x79\xcd\xed\x21\x29\xf5\x81\x96\xdf\x06\xc9\xb5\x8e\xa4\x79\x6f\xae\xad\x4f\x5a\xdf\xf4\xcf\xcc\xcc\xbe\x1a\xb2\xfa\xd9\x18\x07\x97\x30\x36\x76\x30\xb6\x7a\xdd\x57\x3d\x50\xc6\xf7\xda\x95\x77\xcc\x39\x39\x43\xde\x94\xed\xf1\x06\x75\xf0\x06\xd7\xd5\xd3\x62\x9a\xfe\xba\xad\x2a\x9a\xb8\xd7\xe0\xaf\xad\xbc\x57\x91\x75\xda\x6e\xbd\xca\xbe\x7c\xa1\x17\x99\x99\x1c\x2f\x3a\x17\x68\x3b\xde\x7d\x5a\x87\x36\x42\xce\x17\x55\xbc\x5f\xf6\x7c\x6c\x11\x9e\x69\xb0\xc6\xf9\x5e\x08\x54\x68\x51\xe9\xeb\x72\x98\x20\x13\x6e\xef\x41\x3e\x1f\x98\x75\xec\x69\x27\xbf\x70\x30\xdd\xc2\xb5\xa6\xb5\x7f\xb2\xda\xda\xfd\xb3\xc1\x67\x38\x4e\xdd\xf1\xfd\x97\xe6\xfd\x10\x8b\xc9\xab\xa7\x5d\xb5\xde\xf5\xf5\x7b\xa8\x27\x72\x07\x2e\x07\xb0\x40\xaf\xf6\x77\x8e\x7b\xe7\x42\xcb\xb0\x7b\xe2\x73\x9a\xdb\xf5\x65\x97\x74\xa7\x0d\x2b\x3b\x17\x8d\x17\x49\x26\xf2\x9e\xc6\x5c\x12\xe1\xf3\x97\xcf\x3b\x8f\x30\x94\x0b\xd1\x73\x8a\x26\x77\x7f\x02\x94\x2b\xac\xd2\x49\x14\x95\x45\x86\x7e\x96\xaf\x34\xb6\x33\x0f\xca\x37\x9a\x8d\x67\x66\xb5\x0c\x52\x14\x75\x6d\x07\xe8\x1b\x2e\xa7\x68\xd7\xc9\x8a\x5d\xa1\xef\xb7\x25\xef\xb7\x71\xd8\x8c\x45\x55\x28\x2d\x15\x3d\xcc\xf3\x79\xac\x10\xfb\x59\xaf\x0f\x6a\x52\x6f\xe7\xe1\xa1\x49\xae\xab\x92\x69\xf0\x17\x5d\xca\xcd\x14\xa8\xa8\x38\xdf\x8b\xad\xa5\x4a\xfb\xfa\x3e\xa1\x26\x47\xc9\xba\x95\xb0\xa0\xe9\x95\x63\x7a\xd9\x9d\xd9\x31\xe1\xac\x2a\x74\x51\xd5\xb1\xd7\xe7\xed\xba\xb6\xd7\xf3\xc5\x33\xae\x4f\x80\x53\x12\xa2\x19\x1c\xa3\x87\x8e\x6b\xcd\x09\xe9\xbe\x0e\xdb\x04\xeb\xe8\x62\xa4\xcf\xa7\x2a\xb1\xe2\xc0\xfb\x44\x0f\xbd\xd2\x19\xb0\x3f\x7b\x97\x55\x88\x57\x7c\xde\xeb\x31\xfe\x28\x9a\x2e\xd1\x4b\xca\xc9\x39\x5c\x6f\xfd\x5f\x75\x5a\x8b\x53\x94\x6d\x5e\x62\xb8\x81\x12\xa6\xb1\xec\x8a\x55\xaf\xa1\x12\xe5\x88\xd5\x77\xab\x6f\x78\x71\x51\x96\x2e\x4e\xb1\xe6\x49\x32\x96\x23\x68\xa6\x6e\x27\xc8\xf4\xeb\x8d\xad\xd9\x68\x50\x69\x89\x69\xe5\xc9\x9b\xe3\xdb\xf4\xf9\x0f\xd2\x6a\x3e\x35\x1c\x39\x2e\x0e\xd5\xd6\x1c\x39\x93\x3f\x14\x58\x0c\xe4\x92\x2d\x9f\x56\xab\x2e\x6d\xdf\xf4\xb6\x8f\x65\x66\x8e\xbe\x0a\xaa\x35\x11\x2f\x23\xad\x10\xbb\x89\x79\xd9\x78\x65\xaf\xb2\xbe\x37\x24\xb7\xd0\x8f\x15\x0d\x76\x3e\x5e\xae\x18\x9d\xd8\xb6\xaa\x67\x28\x07\xf8\xb8\x92\x17\xe1\xb2\xd2\x64\x6e\xf0\x31\xd3\xba\x51\x99\xc1\xdb\x6a\x80\xa1\xa8\xf1\xb3\x59\x3f\x40\x53\xf6\x6c\xb3\xe8\x41\x4e\xf0\x8c\xfe\xd6\x6d\xc0\x74\xb6\x95\xc9\x5b\x6d\xad\x0a\x5d\xf9\x90\xe0\xf8\x99\xbe\xc4\x63\x99\xab\xec\x06\xae\xe4\xb5\x09\x4f\x6c\x64\x3b\x35\xa7\x4a\x67\xf9\x47\x67\xf2\x66\x1d\x29\x9e\x88\x18\xbf\x98\x05\x31\xfb\x4b\x5c\x77\xad\x0d\x0e\x67\x8a\x21\x06\xb1\x70\x03\x0e\x3b\x8c\x5c\x65\xef\x1e\xa2\x28\xb6\x65\xae\xfa\x2b\xbd\xb5\x01\xd0\xed\x80\x4d\x5e\xa7\xd4\x39\x34\xbf\x7a\x66\xe4\x23\x2f\xaf\xf9\x96\x20\xfa\x74\xb6\xcf\xc4\xfb\xe9\x2b\x21\x0d\x53\xe0\x07\x26\xfb\x5d\xa1\xb1\x5c\xa0\x90\xc3\x74\x1a\x57\x71\xf6\xba\x49\x1f\x77\xe7\x14\xe4\xb1\xc6\xd7\x46\xb7\xe9\xed\x42\xa7\x0e\x33\xe1\x78\xc6\x94\x34\xdd\x0e\x97\x4b\xeb\xb3\x6b\x71\xa8\x8c\x40\x75\xe4\xf2\xc2\x9b\x87\x9a\x82\x19\x49\xc6\x5f\xad\xa4\x3f\xac\x3a\xfa\x44\x16\xf6\x60\xb2\xc2\x57\x5c\x7b\xd9\x51\x72\xd3\xc4\x3b\x3f\x16\x8f\x9e\x3c\xb4\xdf\xb3\x5b\x3d\xae\x3d\xaa\xd9\xa8\xb0\x3d\x3f\xd3\x52\xaa\x71\x92\x29\x8b\xe6\xa5\xeb\xda\x08\xa7\xb7\x9f\x6e\xd8\x75\x08\xed\x37\xe2\x15\x3a\xf1\x33\x7a\x69\xec\xc5\x56\x58\x37\xd2\xf5\x96\x5e\xd4\x7f\x1d\xc4\x28\xd6\x43\x62\xe3\x4f\xcd\x82\xba\xbe\x7d\xec\xd5\x7e\x4d\x48\x44\x44\x18\x95\xcc\x2a\x50\x6b\x23\xef\x35\x80\x11\x17\xae\x60\xf1\xa0\x2a\xd1\xda\xf1\x90\x95\x8f\xae\xd3\x28\x58\xbf\x18\x6c\x45\x8e\x52\xc7\x57\xd5\xe1\x4e\xdd\x86\xf7\x3f\x8f\x2d\x7c\xd8\xc1\x8f\x4c\x1d\x94\x71\x30\x72\x7e\x7f\x53\x3e\x98\x31\x79\xbf\xc2\xe3\x4d\x8e\xc4\xf8\x7b\x70\xf3\xb5\x97\x2c\xfa\x61\x50\xd5\x42\x20\xa8\x30\x5b\x76\x85\x38\x84\x34\x24\x37\x61\x3e\xc8\x4b\x6b\xda\x6d\xad\xcb\xf2\x0f\xfc\xe9\x3d\xbd\x9b\xe7\xa5\xd2\xbe\x85\xbc\x94\xf1\xa6\x3c\x2d\x13\xf0\xde\x54\xfa\xde\xe1\x1c\x55\x4a\x5c\x7a\xd3\xf4\x13\xe3\x7b\x61\xed\x22\xfe\x29\x1f\xd1\x77\x03\x70\x89\x21\x9b\x6a\xdd\x42\xe3\xd0\x05\xb7\xa3\x2f\xe8\x55\x17\xca\xe7\x83\xba\x56\x7d\xcf\x37\xc2\xee\xb1\x29\xd9\x78\x69\x93\x1c\x28\x0e\x5c\xcf\x67\x0a\xb5\x14\x92\xad\x0b\xdf\x24\x9c\xb7\x13\x6e\xa3\x58\x16\x69\x89\xa6\xf0\xce\xd4\xfb\x2a\xda\xf2\x82\x4c\x1f\xb0\x45\x6c\xab\x0c\x39\xb7\x03\xb3\x0e\x28\x79\xc3\x79\xba\x52\xae\xd1\xb9\x0c\xbe\xe7\xfb\x48\x75\x4f\xb1\x48\x46\x56\xa6\xa7\x49\xe0\x54\xc3\x9f\x25\x5d\x9b\xc2\xcb\xcf\xaf\x4b\x54\x04\xb0\xf1\xa2\x48\x44\x47\x84\x89\xc6\xa9\xa8\xbb\xfb\x2d\xde\x72\x84\xab\x6a\xf2\xad\x75\xbc\x17\xa5\x66\x72\x85\x4c\xb9\xf4\xd3\x2f\xf7\x87\xf0\x13\xdf\xb2\x29\xa5\xfd\x1c\xb7\xac\x69\xb5\x3e\xce\xba\xd2\x32\xac\x97\xc8\x77\x64\x5f\xe8\xa7\xe5\x33\xb0\x98\xf9\xf1\x65\x4f\xf3\x94\xa6\x05\xbf\x13\x6d\xd9\x1a\xa2\x12\x79\x07\x43\x7b\x02\xb8\xb5\xf7\xd1\xb4\x2c\x12\xa9\x75\x4a\x53\x63\x47\x23\x01\x66\x7e\x1e\xb2\xa9\xd9\xca\xa0\xbf\x6f\xd7\x74\xf7\xd3\x89\xd1\x74\x4f\xa8\x79\xa0\xfb\xea\x56\xf9\xe9\xad\xea\xcd\xe8\x7d\xad\xfd\xe2\x8c\x34\x0e\x53\xdf\x77\xf6\x30\x1e\xcb\x23\xc7\xd0\x57\x01\x9a\xb4\x86\xeb\x37\x88\x8d\xf2\x0d\xc7\x83\x1f\xa9\x4e\x9f\x37\x80\xf5\x24\xd8\xf7\x54\xa8\x7c\x17\xd7\x37\x2f\xf4\x34\x61\xad\x08\x21\x51\x73\x0d\xfb\x95\x82\xbc\xcd\xc6\x42\x35\x13\x51\x9a\x87\xd7\x64\xc9\x3c\xa8\x68\x2e\x19\xc2\xac\xc6\x2d\x27\xb7\x58\x38\xbe\xd6\x60\xa2\x69\x9f\x4c\x0c\x58\xb5\xa1\x9c\x0f\x82\x69\x79\xca\xe7\x36\x90\x97\x7a\x9f\x13\x7c\x43\xf4\x22\xf5\xbb\x42\x2d\xe6\x88\x77\x96\x22\x4e\xcc\x76\x65\xfb\x8e\xdc\xb4\x66\x9c\x14\xb3\x07\xc3\x12\x68\xdf\x86\x8d\x97\x58\x9d\x9a\x34\x5b\x47\xf6\x8b\xe3\xe9\x45\xf9\x1f\xfb\x2d\xad\xaa\x9a\xbe\x8e\x19\xc1\x85\x77\xa6\x34\x5e\x14\x0f\x87\x52\x05\x1c\x54\xbe\x23\xdc\x63\x55\xbc\x73\xbb\x10\xad\x5e\x62\xe9\xbd\x72\x23\x34\x94\x04\xc2\xd6\xd5\xdb\x05\x62\xc7\x34\xea\x9e\xb4\x8d\x36\xd8\xb0\xf0\x19\x97\xe4\x7d\x18\x8a\xea\x22\xae\x12\xea\xda\x92\x9f\xdc\x69\xbb\x9b\x40\x60\x8e\x1e\x13\x34\xaf\xa9\x62\xf0\x32\xb2\xc6\x09\xb0\x8b\xb8\xb5\x89\x26\x1d\x28\x08\x4c\x5b\x44\xae\xa8\xaf\x17\x2a\xbd\x35\x8d\x8c\x4d\x72\x28\x7e\xe7\x25\xba\x6d\x2d\xe0\x98\x84\xee\x61\x2b\x55\x97\xe9\x1f\x0c\x17\x85\xe8\x44\xaa\x0b\xf4\x75\x5f\xab\x6f\x16\x78\x65\xf1\x34\xcb\xf8\x31\xbc\xfd\x85\x4c\xca\x8b\xe0\xa1\xae\x53\x70\x79\xa4\xdd\x71\xfc\x96\x73\xb7\x6c\x64\x4f\xbb\xdf\xf6\xad\xb4\x57\xc5\x4e\x57\xb9\xa7\x12\x7d\x9e\x19\xe1\x55\xc3\xcb\x8a\x5b\xbd\x56\xac\x7e\x4e\x0b\xb4\xd6\x01\x66\xda\xf3\x0f\xe4\x42\x76\xcb\xfc\x34\x58\x19\x90\xf7\xa9\xe9\x08\x72\x11\x94\x7c\xad\xc3\xf2\x13\x0f\x2a\xfb\x0c\x91\x12\xcf\x2a\x3c\x00\x79\xde\xeb\x23\xc2\x4d\x20\x8f\xe1\x23\xef\xa9\x31\x1a\x96\x89\x94\x64\x9f\xdb\x1b\x1f\xc8\xc0\x0a\x3e\xa1\x75\x68\x66\x78\xda\x83\x21\x7d\xed\x82\x74\xbf\x82\x94\x35\xb2\x8d\x5b\xb7\x2f\xd4\x97\xbc\xab\x2c\xba\xe0\x52\x15\xbd\x5c\x76\xe0\xfd\xe2\x24\x69\xd4\xc2\xef\x24\x69\x14\x76\x03\xbf\x31\xf5\x69\x71\x63\xec\x93\xa0\x0c\xb1\x90\x4b\x71\x2c\x6e\x52\xa7\xf7\x87\xcb\x12\xbc\x54\xd5\x6f\xa9\xad\xa7\xae\x67\x38\x65\x26\x47\x31\xeb\x4e\x14\xb9\x1d\x41\x33\xc0\xaf\xe3\xcd\x61\xc0\xdb\xa4\x80\x2c\xef\xbe\xc2\x26\xbf\x43\x17\x9d\x17\xa2\x9b\x5a\x13\x06\xda\x81\xe3\x05\x6b\x29\x53\x6c\x3f\xff\x42\xd9\x1c\x4d\xce\x75\xab\xba\x4d\x53\x4e\x78\x79\x06\xf1\x73\x87\x1b\xba\x41\x9c\xaf\x70\x53\xd4\xb6\x4c\xcb\xca\x02\xd6\x2e\x09\x1b\xdf\xf2\x30\xb1\x3b\xb7\x55\x7e\xb5\xef\x41\x7d\xc4\x8c\x9a\x2b\x6a\x94\xae\xe5\xae\x73\x0b\xeb\x87\x07\x7e\xca\xf2\x53\x82\xf7\xfa\xcb\x7a\xb7\xd2\xe7\x54\x3f\x28\xbf\xc9\xb7\x76\xb0\x72\xdb\x59\x76\x79\x56\x86\x8a\x6a\xbd\x57\x94\xdc\x51\x37\x8a\x71\x96\xc8\x34\x77\x2f\x1f\xdc\x65\x0c\x7b\x93\x7c\x5e\xc5\xc3\x38\xf7\x04\xd7\x4c\xc7\xb1\xec\xb3\x5b\x40\xc7\xd1\xd9\xfd\xc4\x03\x4d\xaf\x3e\x97\x4b\x97\x36\x0b\xfe\x11\x42\x97\x96\xcd\x34\xba\xea\x2a\xc7\xb4\x80\xe4\xaa\xd1\x5e\x3f\xcf\x25\x71\x5f\xb9\x35\xda\x9d\xd0\xcf\xf1\x20\x6b\xcd\xf5\xca\xcb\x2b\x2e\x52\x66\x97\x68\x94\x6f\x44\x4f\xb7\x4c\x1f\xc9\x0a\xd2\xad\x80\x65\x47\x39\x00\x24\xe3\x73\x95\xd9\x86\xe1\xca\xc6\x4e\x5b\xcb\x65\xe7\x0d\x59\x73\xa4\x9c\x1f\x28\x87\x51\x91\xd0\x71\xab\x7e\x26\xe1\x9a\x5b\x99\x73\x54\x52\xce\x61\xd8\x0f\x81\x83\x73\xfe\xc6\xb6\x3f\x32\xe8\x4c\x1f\x8c\xee\xbc\x94\x32\x3b\xb7\xd6\x4b\x58\x75\xe5\x5d\xef\x7c\xe2\xf1\x9a\x7a\xef\x0b\xcf\xe0\x9c\xb5\x71\xc6\x4f\x4b\xf1\xc1\x72\x07\x29\x5f\x34\x67\x82\x82\xa3\x49\x92\xca\x7c\xa4\x62\xe7\xb2\x31\xbf\xb8\x38\xc7\x5d\x89\x12\x47\xb3\x71\xa1\xea\xf7\x0b\x36\x0b\x98\x42\xc9\x4c\x8d\x77\x89\x8e\x52\xec\x71\x08\xe3\xd4\x57\x2c\xa6\x02\xdf\x13\x22\x3d\x11\xe6\xd7\x3d\xde\xa9\xb0\xbf\xc9\x39\x6e\xbd\x7b\xb7\x10\x94\x1e\xf3\xf0\x68\x7e\x21\xbb\xa4\x30\xde\x55\x8c\x49\x4b\xfd\xfb\xd0\xc7\xf5\x62\xb7\xc8\xce\x4d\xc0\x75\xbd\x98\x8a\xa2\xec\xf2\x72\xcc\x47\xec\x15\x09\x9d\x7e\xfe\xa5\x42\x65\xbd\x04\x91\xbc\x44\x47\x21\x99\x6c\x41\x42\xc2\xfc\x73\xa3\x0f\x7c\x02\x6d\x36\x83\xfa\x52\xe9\xfc\xd6\x70\x41\x5a\xf8\x2e\xe2\x96\xac\xa3\x14\xe7\xed\xdb\x8e\x62\xfc\xdf\xfb\x85\xbf\xb2\x72\xc5\x3e\x7c\x20\xe9\xc6\x9f\xae\x51\xcc\xcf\x79\x47\x55\x68\xf9\x84\x2a\x61\x50\x5f\x48\xaa\xb5\x21\x8f\x7d\x89\x98\x62\x94\xd4\x44\xf2\x86\x8a\xb9\x18\x0d\xaf\xb8\x8a\x70\xef\x14\x0a\x7f\x53\x8e\xff\xae\x6a\xb3\xad\x9f\x41\x02\x33\x70\x45\xde\xa2\xfa\xf8\x98\x09\x90\x9f\x63\x03\xb1\xfe\x2a\xcd\x6f\x5e\xc8\x77\xc5\x28\xcf\xcc\xba\xfc\x80\x3f\x25\xfd\xd2\xa8\xce\xd4\xf3\x4e\x57\x11\x86\xd8\x58\x35\x61\x8b\x94\x97\x35\x8f\x1f\x16\xe0\x92\x77\x95\xd0\x19\x31\x0f\x0b\x16\x16\xc2\x0b\x3c\x18\x2f\x0e\x4d\xa8\x36\x4a\xb1\x6a\xaa\x76\x6c\x2b\xa1\x05\x3c\xc2\xe9\xd5\x1b\xb7\x2f\x22\xf6\x1c\x14\x91\x4c\x45\xbd\x42\x06\x86\x51\x5a\xfc\x09\xdb\x6a\xdc\x36\x01\x8e\xfc\x6c\x74\xed\x1d\x29\xec\xba\xfb\xca\xed\xcf\x71\x15\x2c\x3a\x89\xab\xd4\x06\x31\x5d\x2c\x5c\x6a\xdb\x51\xda\xbc\xb9\x0f\xd8\x8b\xf7\x79\x6e\x7d\x60\xc4\x44\xed\x86\xd5\x94\x76\x07\x33\x59\xd5\xa4\x16\x1e\x1c\xf0\xfb\xd8\x0d\x95\xbb\xa2\xdf\xc9\xe8\x8e\x25\xce\xb9\xe9\x5e\x6b\x5f\xd8\x3e\xd0\x14\xcb\x1a\xf4\x4e\x20\xa3\xcd\xe3\xfb\xbe\x73\x8d\x27\xb6\x7d\x28\x97\xfd\xc9\x88\x76\x04\x43\xbe\xe3\x7b\x55\x3a\x8d\x32\x39\x9d\x94\xe1\xba\xd9\x91\x78\xdb\x74\xef\x48\x37\x84\xa6\xa1\xa0\x2e\x6d\x3c\x49\xaa\x5c\x47\x87\x8e\x5b\x66\xec\xd9\x6a\x09\x5b\x76\x1f\xfc\x11\xf6\xaf\x3b\xdb\x43\xe1\x0c\x98\x46\x1e\x35\x3b\xac\x75\x43\xf4\x04\x8f\x4d\x89\x74\xcb\x71\x77\xc2\xf2\xda\x72\x82\x27\x7f\x81\x5c\x09\x53\x7a\x2e\xcd\x88\x5a\xdd\xa1\xd9\x78\x99\xae\x97\x3c\x36\x46\x70\xa8\x30\x4c\x9f\xd4\xc0\x9b\xc9\xa9\xe7\x59\xe5\x29\x4f\xa3\x84\x89\xe9\x83\x67\x84\xf3\x5e\xe2\x3d\x32\x62\x23\x4b\x72\x4d\x7a\x76\x25\x42\x91\x8c\x4e\x3e\xe1\xb6\x72\x84\xc8\xa0\xe2\x2c\xb5\x58\xfe\xb1\x88\xc3\x83\x24\x92\xfa\x80\x8a\xae\xdb\x7c\x5c\x62\xd3\x85\xde\x0b\xd7\xd5\xa2\xcd\x0d\xf0\x28\x3a\xc8\x84\x96\x78\xfa\x4e\x7e\x49\xc9\xb5\x02\xa3\xb8\x9c\x0c\xa7\xd1\x2b\x23\xf2\x47\x7c\xb6\xd9\xd1\xbb\x7b\x57\xbc\x9a\xcc\x2f\x1e\xa1\x05\xd4\x27\x4b\xe2\x84\x16\x72\xeb\x99\x75\xf9\x79\x04\xf1\x11\x3b\xfc\x05\xf9\x3b\xfd\xb8\x9f\x92\x4d\x70\xfb\xc0\xab\xf9\x79\x4b\xb6\x86\x3a\xce\xb1\xbb\x38\xe0\xb2\x89\xa1\xc4\x64\x41\x56\xa5\x9b\x0b\x41\xa7\x42\x64\xbb\x2b\x5f\x32\xfc\xfd\x0d\xf9\x7d\xfe\x29\xa5\x2a\x74\xdc\x7c\xdc\xa8\xea\xb5\x3a\xf0\xa6\x5d\x14\x1a\xf3\x30\xf4\xb5\x81\x9d\xdf\x7d\xfe\xa3\xd7\xfa\xf4\xa3\xb6\xdf\xd5\xdc\x09\x10\x89\x8f\x81\xb4\x71\xd2\x1d\x0b\x42\x2b\x71\x5b\x7d\x4b\xfc\xaa\xaf\x5d\x90\xbc\x77\xbb\xdd\x82\xf9\x7c\xf3\xf6\xdb\xce\x67\x3a\x45\xf1\xa7\x58\x48\xd4\x32\xf9\x9c\xda\x5a\x96\xca\x8a\x06\xc8\xbc\x51\xfb\xea\x2e\x9d\x0f\x93\x79\xef\x23\xd7\xaa\xe7\x6b\x31\x9e\xfa\x7d\x47\x68\x5e\x09\x5b\x8d\xbe\x15\x0f\x6e\xbe\x42\x57\xdc\x90\xc9\x5b\xda\x6b\x27\xf1\xc2\xe9\xeb\x73\xe1\xc7\xa7\xb9\x3c\x6a\xa3\x06\xdf\x9b\xde\xbd\x8e\x53\x93\x55\x7f\xa1\x97\x3f\xea\xf7\xd8\x52\x6f\x8a\x10\xe3\x7f\x4b\xa6\x69\xaa\xa7\x67\x5f\x7c\x5b\xaa\xb0\x61\x75\x5c\xeb\xce\x65\x54\x2f\x56\x35\x42\x4e\x2b\x4b\xf1\xa8\x28\xed\x59\x91\x6d\xc0\xc6\xf0\xe8\x47\xf9\x26\x75\x3e\xdf\x0f\x8c\x53\xa5\x4a\x71\x30\xcc\x7c\x59\x61\xc4\x4e\xd3\xfb\xa9\xdb\xde\x71\x20\x23\x01\x69\xd4\xf3\xa3\x1e\x75\x33\x94\xbc\x6b\xba\x7d\x7c\xb1\x8d\xec\x26\x6d\x37\x7f\x0f\x1f\xf6\x51\x97\x57\x70\x40\x2e\xa4\x2f\xc0\xde\x26\x7a\x6b\x45\xdb\x87\xd0\xe6\xfb\x6a\x58\x26\xc4\xac\x51\xc1\xf7\x85\xd8\xea\x85\xfc\x91\x90\xac\x7e\x0a\xd3\x9f\x98\xc6\x17\xa8\x81\xa6\x41\x0f\xcd\xe8\x71\x40\x5f\x56\x2f\x93\x25\x22\xd1\xb7\x4f\x72\x12\xc2\xed\xe2\x20\xd1\xee\x5c\x9d\x32\xf2\xfa\xab\x70\xda\x36\x7a\x26\x0b\x9f\xba\xd6\x2f\x94\x55\xeb\xac\xac\x38\x65\x72\xc2\x53\x78\x28\xf4\x4a\xd3\x64\x9f\xfd\xcb\x95\xd0\xe5\xc4\x0d\x6f\xcf\x9a\xf9\x5c\xbf\xfb\xfb\x7c\xf5\x99\x15\xc3\xe9\xa7\x4a\x7f\xe9\x5b\xcc\xbc\x2e\x7d\x02\xa4\xdb\x49\x8a\xc5\x06\xbf\x8a\xa3\xce\x6a\xb9\xe0\x9c\x4b\xa5\x15\x9a\x63\x93\x45\x29\x17\xbe\x63\xd4\x1b\xd1\x64\xc7\xbe\xfb\x34\xa9\xd9\x38\xee\x7d\xd2\x2c\x73\x7a\xb3\x5c\x82\x7b\xb9\x86\x76\x8c\xf9\xd1\x6a\x5c\x6f\xc9\x1b\xf2\x9d\x4f\x84\x52\xc6\x80\x2a\x2a\x8f\xed\x93\x6b\xd9\xfd\x77\x8a\x2d\x24\x57\x44\x68\x9c\x3f\x9a\x35\x06\x96\xf2\x07\x8a\xbe\xea\xba\x26\x1d\xab\xd3\x3a\x2c\x93\xc9\xf4\x0a\x4b\xde\x8f\x89\x39\x90\xee\x50\xbc\xea\x57\xb3\xd4\xfe\x92\x23\x33\xdc\x89\xd3\x94\x6f\xb6\x15\xcc\x99\xbd\x03\xcf\xb8\xa6\xe2\x7a\x89\x85\xc1\x85\xf5\x8d\x1b\x35\xa5\xab\xa8\x59\x9c\xc5\x1b\xbf\xf3\x83\xaa\xfb\x76\xad\xe3\x8d\xe7\x5f\x85\x0b\x27\xd4\x67\x2e\x9b\x52\xcf\x68\xec\x6a\x56\x85\x9a\xac\x14\x0d\x8c\x2b\x7d\xb9\x22\x45\xfb\xe8\xce\xe8\x4b\x0d\xbe\x4e\x21\x92\x11\xa9\x21\x73\x4b\x6a\x48\x52\xd3\x68\xf1\xfa\x73\xe4\x95\xa4\xcf\x2b\x6a\xdc\x93\x2b\xd3\x2f\x66\x4c\x23\xbf\xe6\xd7\x5b\xb6\x4b\x53\xe7\x4b\x7b\x0f\xbe\x3b\x5d\xf9\xb6\x36\x3a\x38\xeb\xd2\xbe\xf3\x64\x6b\x57\xe0\xc1\x6d\x07\x1d\x39\x85\x03\xea\xe8\x7b\xf7\xdc\x62\xd7\xf2\x33\x69\xc8\x6f\x6c\x37\xc8\x9d\xbb\x2a\xf4\x89\x26\xf6\xaa\xaf\x84\x86\xf9\x80\x04\x2b\xe8\x19\x7a\xeb\x8d\xf1\x52\x54\x73\x8c\x22\xa8\x79\xa7\xa2\x6c\x01\xeb\xec\x4b\x67\xde\x7f\xb2\x2b\x39\xb7\xbe\xee\xb3\x69\xd4\x67\x1a\x65\xe2\xd7\x47\x9b\x75\x3b\x9d\x31\x0b\x67\x3a\x97\x13\xa8\xdc\x95\x92\x0d\xdb\xcf\x90\x0c\x58\xca\xb7\x8f\xd9\x6e\x56\x5d\x4b\x2c\xbd\x1e\x7a\x90\xb8\xec\x6b\xdd\xce\x58\x79\x27\x22\x9f\x7b\xa7\xee\xe5\xf5\xb8\x74\x26\xad\xac\x77\xef\xd4\x9c\xa5\x22\x6b\xc2\x73\xc3\xd9\x79\x18\x18\xe9\xe7\xae\xf2\x98\x2d\xea\x36\x0d\xec\x52\xbc\x3d\x94\x6c\xf8\xe9\xd0\xe1\x71\x64\xd1\x60\x1f\xde\x4d\xa1\x1a\x1e\x07\xb8\xfe\x30\x67\x4d\x28\xf3\x40\xec\x5e\x98\x50\xbc\x5a\x15\xd7\x23\x3a\xea\x95\xb9\xbb\x46\x8e\x0e\x9f\x92\x80\xed\x6e\x52\x36\x2e\x95\xa5\xea\x77\xde\xdf\xef\xe4\xa3\x5b\x8c\xac\x94\x60\x99\xad\x55\x61\x97\x29\xf9\xb2\x3f\xa7\xcc\x1c\x1e\x6e\x58\x1f\xf6\xf4\x6d\xe3\xf2\xe7\x34\xcb\x11\x5f\xdb\xfe\x00\xbf\xa6\x26\xd5\x29\x59\xe3\x35\x5d\xca\x2b\x94\xe1\x54\xa1\x8c\x21\x9b\x74\x80\x52\x79\xb3\x59\x61\x36\xb0\xee\x12\x65\x85\x2d\xb3\xb9\x75\xe2\x6b\xc5\x98\xe8\xfd\xc8\x2f\x4a\x89\x75\x84\xb4\xc9\x8f\x8a\x76\xf3\x9f\x55\xdf\x6c\xae\xb1\xd4\xd6\x76\x96\xba\xb8\x24\x74\x4f\x3c\x9c\xde\x23\x73\x72\x12\xa5\xb0\x76\x71\xaf\xbe\x18\x3d\x91\x41\x8e\xe5\x6e\xbf\xcf\x55\x42\x8e\x8c\xf3\xb8\x79\xd3\x31\x8a\x8a\x92\x69\xed\xa5\x41\x99\x49\xf8\x4b\xd5\x97\x39\xcc\xa9\x37\xd1\x3a\x61\x2a\x32\xd1\xd7\xed\x8e\x8e\x1d\x9d\xa4\xfb\x53\xed\xfc\xa4\x11\x7a\xfd\x78\xda\xcd\xa1\xa2\xa3\xb1\x8f\xf5\xee\xda\xdd\xf5\xb7\x43\x82\xde\x7e\x0e\x62\x7b\xe7\x17\xa2\x1a\xc7\x43\x35\x33\xaf\xe5\x62\x60\x79\x93\xc3\x82\x73\x7d\x44\xe1\x82\x37\x1b\x58\xf9\xbe\xcb\xb6\x06\xdb\x41\x70\xa1\xfe\x9b\x64\x9e\xa7\x45\x25\x9a\xa3\x64\x19\xd5\x7e\x7a\x9c\x77\xf2\xe8\x2f\x27\x87\x4d\x4c\xe8\x47\xdd\x1e\x50\xbb\x25\x15\xaa\x74\xeb\x8a\xac\xb2\x57\x30\xee\x2f\xb0\xa8\xb0\x46\x1e\x6e\x58\xcd\x46\x3c\x77\x2c\x45\xf0\xc7\x39\x3d\xbb\xa5\x88\x32\x94\xf2\x65\xe1\x81\x8b\x77\x9f\x54\x4f\x89\xed\x27\x2b\x8b\x3a\x88\x9f\xd0\x8b\x5e\xf0\xaf\xfd\x2f\xbb\xe0\xc8\xbf\x77\xbc\x11\x28\x49\xf0\xbf\x40\xd4\xbf\xb7\xc1\xf5\x7d\x5c\xb1\x20\x15\x02\x9e\xa4\x84\x75\xb7\x21\xe2\x5c\x49\x04\xe2\x2f\x53\x0b\xe3\x82\x05\x29\xc8\x9f\x5d\xa2\x1a\x38\x6b\x2c\x11\x43\xc2\x11\xf0\x7a\x58\x22\xce\x4e\x5c\x81\xe0\x6c\x0b\x00\xa9\x38\x63\xec\xdd\x81\x70\xc0\x2f\xba\x82\x02\xc1\xdb\x54\x1c\x01\x87\x01\xc5\x61\x60\x18\x10\x02\x83\xc3\x80\x10\x30\x18\x65\x0e\x52\x23\x61\x9c\x71\x36\xf2\x78\x7b\x67\x2c\x10\x0c\x00\xc9\xbb\xdb\x60\xf1\x24\x20\x5a\x12\x02\x00\x9d\x85\x3c\x33\xc4\xa1\x10\x24\x00\xa4\x88\x71\xbd\x85\xc5\xd9\x3b\x90\x7e\x7d\x08\x00\xe9\x91\xb0\x2e\x86\x40\x34\xf8\x77\x00\x15\x9c\x33\x16\x0a\x44\x00\xc1\x40\x5d\x80\x9c\xdc\xbf\x2a\x41\xff\xb1\xa1\x0f\x45\xc3\xfe\x97\x03\x0b\xf3\xf8\x7e\xfc\x03\xe0\xe5\x0f\x9b\x32\x14\xed\x1b\x18\xbe\xd5\xd5\x9a\x48\xc8\xb8\x23\x72\x7b\xe0\x46\xe1\x06\x39\x30\x26\x39\xf7\x9a\xbc\xcc\xc1\x5e\x28\x66\x52\x50\xd6\xe1\x5e\x9b\xff\x0c\xfa\x1e\xbf\xe6\xa8\xa7\x49\xdd\xe2\x47\x89\x1c\x2a\x55\xd5\xbc\xbc\x50\x8f\xf1\x97\xe7\xa4\x5a\x72\xe1\x6a\x30\x91\x24\xd7\x5a\xc0\x8c\xf5\x52\x18\xff\xf4\xe4\x66\x71\x48\xdd\xf0\x82\xae\x01\xbf\xcf\x4e\xe3\xc7\xf1\x0c\x6e\x09\x22\xdb\xec\x43\x7e\x90\x61\xb0\x81\x6f\x4e\x78\xdc\x8b\x7d\x8f\x66\xb6\x1d\x0a\x98\x83\x2a\x5b\xa1\x93\x08\x66\xd7\x3d\xf3\xb5\xcf\x12\x8c\x7f\x59\xca\x73\xa8\x36\x69\x32\x6e\xfa\xdb\x75\xef\x19\xda\x81\x4b\x6b\xf0\xd2\x6b\x2c\x17\x8b\xe7\xc8\xc4\x46\x19\x1f\x45\x95\xa7\x1a\x8e\x3a\x69\xde\xec\x0e\x6c\x84\xc7\x25\x63\x98\xb2\xa6\x7c\x71\xab\x79\x31\xcc\xf9\x13\xee\x61\x66\x2c\x41\xee\x8b\xee\xdc\x07\x4c\x6c\x08\x0a\xe2\x07\xb2\x27\x3a\x33\x94\x72\xaa\xb4\x99\x33\xed\x3b\x82\x5b\x8a\x8b\xf0\x2d\x01\xc7\xae\x96\xb8\xf8\x6d\x53\xcd\xf4\x48\x77\x93\xc5\xf4\xb9\x6f\xb5\xe2\x6d\x84\x07\xe9\x81\xf3\xd9\xd6\x95\x9f\x54\x43\x9e\x04\xa5\x3c\x80\x3f\x0e\x9b\x39\x86\x46\x5f\x94\xf0\x6f\x3a\xff\x91\x3f\x32\xe7\xbf\xc8\x47\xf2\xbf\x28\x05\xa4\xe7\x61\x4d\x3a\x33\xf4\x89\x1e\xd8\x5f\xa8\x02\xc6\x1d\xfb\xcb\xf3\xff\x17\x0c\x8e\xe8\x4e\x52\x74\xc0\x10\xcf\x74\xa0\x81\xf9\xfb\x1d\x02\x03\x80\x8c\x70\xb6\x24\x07\x77\x53\x14\x0a\x05\x44\x22\x91\x40\x38\x1c\x06\x84\xa1\x25\x81\x30\x18\x0c\x08\x45\x80\x81\x68\x18\x0c\x88\x40\x20\x81\x50\x14\x0a\x88\x00\x83\x7f\xdf\x08\xe4\x2f\xff\xd9\xbb\x39\xe0\x1f\x0a\x06\xa2\x7e\xe9\x04\xa4\x4f\x30\xc0\xe3\xce\xc6\x0f\x44\xff\x53\x38\x10\xf0\xbf\x2b\x53\x81\x00\x25\xff\xf0\x43\xfe\xc3\x4f\xc0\x9f\x89\xf2\xf7\x9a\xda\x44\x82\x8d\x1e\x96\x64\x0a\xd2\x56\x52\x01\xe9\x63\xbd\x49\xe6\xff\xe3\xb3\x7f\xf4\x4b\x1b\x63\x7f\xf6\x20\x9e\xc9\x1c\xfe\xeb\xa8\x49\x17\xeb\x4e\xf0\x20\xda\x60\xdd\x81\xbf\x42\xe8\x82\x34\xb1\xb6\x38\xcc\xd9\xcf\xe7\x2c\x00\x42\x12\x21\x01\x45\x21\x10\x92\x60\x04\x02\x02\x41\x43\x80\x68\x38\x44\x02\x8d\x84\xc0\x21\x28\x18\x14\x8a\x86\x21\xcc\x41\xaa\x44\x82\x87\xab\x8c\x0c\x48\x0f\xa4\x4f\xc4\xe0\xdd\x5d\xcf\x16\xb7\xf1\x01\x29\xea\x81\x94\xb0\x9e\x38\x1b\xac\xae\xaa\x02\x48\x0d\x48\x22\x7a\x60\xe5\xe4\x40\x8a\x04\x3c\x09\x8b\x27\xb9\x03\xa1\x67\xb1\xfe\x23\x51\xf8\x7f\x49\xd4\x1d\xf0\x47\x76\x80\x7f\xa7\x07\xfc\x3b\xbf\xb3\x94\xce\x5a\x7e\x1b\x67\xeb\x6e\x0a\xfc\xc5\x3a\x33\x15\x09\x1e\x67\x5d\xfa\xcf\x5e\x40\xff\x11\x43\x11\x43\xc2\x38\x13\xec\x7f\xc7\xfa\xdd\x0e\x00\xe8\x8e\x2b\x16\x2f\x6f\x73\xa6\x15\xd3\xdf\x6b\x81\x8c\xef\x9a\x00\xf1\x1e\xce\xce\xbf\x1f\x60\xf3\x33\xb1\xe0\xed\x85\xb0\x78\x71\x03\x3d\xe1\xff\xd1\x6c\xd8\xbf\x03\x28\x12\xb1\x18\x12\x81\x28\xa3\xa2\xac\xa2\x02\x06\x23\x50\x60\x30\x0a\x0a\x06\x23\x25\xc1\x60\x14\x1c\x0c\x46\x22\xce\x6c\xb9\x5f\xd3\xb3\xf5\xb0\xc1\xfe\x5f\x1e\x5c\xf1\x37\x07\x09\xfd\x9b\x8f\x00\x83\xe1\x2a\x60\x30\x12\xf9\xf7\x7d\xe6\x83\xfd\xc6\xa1\x60\x30\x18\x86\x04\x83\xa1\xca\x60\x30\x0c\x2c\x07\xf8\x1d\x12\x47\xc0\x2b\x61\x48\x58\x21\x25\x29\x28\x18\x22\x09\x96\x84\xa0\x21\x48\x18\x1c\x8a\x34\xb9\x2e\xfc\x1f\x99\x7a\x13\xb1\x76\x00\x30\x10\x02\x07\x80\xff\x75\x01\x91\x08\x04\x0c\x01\xb4\x03\xfe\x8d\x21\x25\x25\xd1\xc0\xdf\x1e\x3c\xf0\x5f\x3c\x88\xe4\x1f\x18\x14\x8a\xf8\x27\x86\x82\x20\x51\x7f\xf2\xe0\x7f\xf0\x90\x90\x3f\xd7\x43\x42\xe0\xe0\x3f\x30\x18\x1c\xfa\x07\x86\x94\x84\xff\x81\x49\x9e\xc9\xe4\x9f\x18\x1c\xf6\x47\x7e\x50\x24\xf2\x0f\x0c\x86\xfc\x0f\x1e\x89\x88\xc1\x39\x63\x89\x67\xa3\xd4\xc3\xf9\x62\x81\x10\x38\x48\x97\x40\x20\x01\x7f\x89\x48\x17\x00\x52\xc3\xdb\x11\x80\xbf\x06\x7e\x66\x28\x01\x4d\x81\x32\x50\xa4\x82\x22\x0a\xa5\x08\x81\x28\xa8\x20\x20\x28\x28\x04\x06\x56\x41\xca\xc3\x95\xd1\x92\x48\xa8\x0a\x0c\xac\x04\x96\x03\xfc\xef\x94\x33\xe5\x2a\x11\x6c\x14\x1d\xb0\x36\x4e\xee\x1e\x2e\x40\x10\x1a\xa1\xa0\xa8\x20\x0f\x55\x42\x41\x51\x28\x79\xb4\x3c\x4c\x19\x8e\x52\x02\x4b\xa2\xa0\x2a\x28\x25\x38\x5a\x12\x0c\x03\xfc\xfa\x27\xc1\x10\x49\xbf\x46\x8a\x42\xc0\x60\x00\x01\x01\xe5\x3b\x2a\x80\xff\x13\x00\x00\xff\xff\x25\xb2\x5e\xd5\x50\x1f\x00\x00")
+
+func testAssetsTest_expectedPdfBytes() ([]byte, error) {
+ return bindataRead(
+ _testAssetsTest_expectedPdf,
+ "test/assets/test_expected.pdf",
+ )
+}
+
+func testAssetsTest_expectedPdf() (*asset, error) {
+ bytes, err := testAssetsTest_expectedPdfBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "test/assets/test_expected.pdf", size: 8016, mode: os.FileMode(420), modTime: time.Unix(1568824473, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+var _testAssetsTest_templateOdt = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x79\x77\x50\x93\xdb\xd7\xee\x8b\xf4\x8e\x34\x01\xa9\x0a\xd2\xa4\x17\xe9\xd2\x11\xa9\x0a\x08\x86\x5e\x42\x87\x84\x24\x74\xa4\x57\x81\xd0\x11\x10\xa4\x07\x04\x29\x52\x0f\x20\x20\x4d\x90\xaa\xa1\x0a\x02\x02\xd2\x09\xbd\x48\xbf\xe3\xef\xce\xb9\x47\xcf\xd5\xf3\x9d\xdf\x7f\xdf\x9a\x79\x67\x92\x99\x3c\xcf\xde\xd9\x7b\xbf\x6b\x3d\xfb\x59\xba\x1a\xd8\x38\xd4\x00\x40\x00\x00\x83\x4f\x84\x75\xcc\xde\x09\x93\x70\x02\x00\xf0\xfd\x21\x00\x00\xc0\xc5\xc1\x05\x8c\xf0\x86\x82\x2d\xa1\x50\x67\x07\x6b\x4b\x84\x03\xc4\x55\xc0\xc3\xd5\x86\x1f\x62\x09\x77\x80\xf3\x43\xa0\x60\x57\x1b\x88\xb5\xbb\x0b\xd8\x15\xc1\x8f\x00\x7b\x21\xfe\x2f\x19\x01\x01\xc1\x7f\xc8\x80\x1f\xe2\x3f\x64\x60\x84\x25\xbf\x97\x8b\x73\xac\x9e\x56\xdc\x73\x41\xea\x37\x47\x86\xca\x35\x86\x95\xc7\x04\x16\xc4\x13\xc6\x3a\xa6\x2e\x85\x5a\x85\xcd\x2a\xc8\x73\xc5\xcf\x94\xd7\xe9\xed\x3e\x5d\x9c\x66\x0c\x0d\xb8\xc2\xb8\xe6\x03\x97\x07\x04\x21\x2f\x30\xf3\x73\x98\xcf\xce\x66\x6d\x88\x87\xa5\x68\x62\xec\x0d\x17\x79\x37\xbe\x82\x12\x8d\xad\xf1\xcd\xa6\x6c\x5f\xf2\xd1\x67\x8a\x2c\x71\x89\xd8\x34\x19\xe6\x83\x39\xaf\x02\xf0\xfa\xc4\xb5\x4b\xa4\x40\xd7\xd9\x32\xd0\xd2\x8f\xa7\x1e\x86\x9e\xb4\x90\x9f\x48\x7c\x40\xa9\x5e\x7d\x27\x54\x92\x60\x91\x7a\x5c\x41\x79\x52\x31\xf3\x8a\xae\xb3\xd0\x53\xe8\xa9\xe3\xd8\x7b\x24\x8f\x47\x38\x12\xa4\xed\x4e\xec\xf8\x3a\x67\xcb\x44\xaa\x4e\xac\x0c\xd7\xbc\x6c\x32\xda\x41\xf5\x66\xdb\x55\x51\xc1\x48\x29\x6a\x3c\x36\xd0\x1f\x90\x58\xfd\x80\x4d\x77\x5a\x4a\xc5\x0f\x46\xa6\xc5\xa0\xf5\x14\x09\x84\xd0\xa2\xa9\x15\xbb\x3d\xa8\x62\xc3\xed\xa0\xf5\xea\xd2\xa7\x58\xb6\xa5\xad\xd2\x20\x35\xb3\x5a\x4d\xcf\x1e\x10\x8d\xb5\x95\x6b\xe0\xe3\xee\xed\xe9\xfd\x9c\xa7\xd0\x7b\x7e\x99\xc7\x5e\x4d\xc4\xd6\x8d\x5a\xdc\xb7\xb0\x06\x45\x64\xe4\x70\x1b\xf3\xb9\x70\x8a\x0c\x1f\xce\x47\x1c\xf3\xf7\xdb\x27\xa2\x23\x71\x1e\x27\xaa\x1d\x6f\xd3\xda\xe7\x6a\x95\xb3\x16\xb8\x5f\xdf\x8a\x8e\xa6\xbf\x34\x38\xff\xac\x12\x47\xe8\xfd\x82\xcf\xb5\x97\xc5\xe0\x4c\x00\x92\x53\x3a\xaf\xb3\x41\xff\xe2\x0f\x31\x2e\xa8\x95\x4b\x09\xcb\xae\xc5\x17\x07\x12\x24\x9a\x95\x0c\x1a\xef\xe7\x6b\x73\xb6\x7a\x0c\xbe\x4c\x2c\x3a\x15\xfd\x64\x22\x74\x58\xf6\xb1\x35\xc8\x38\xb4\xfe\x53\xc2\x02\xd7\x52\xa8\x45\x82\x2d\xb7\x86\xd5\xb5\xf8\x4e\xf5\xcc\xa2\x08\x3f\xdf\x48\x45\x55\x80\x20\xdd\xd7\xab\xfe\xf9\xc6\xf8\xde\xd4\xe3\x67\x39\xd5\x89\x10\xd5\xcf\x55\x4a\xd8\x72\xe3\xd7\xc0\x1a\x43\x2e\xc3\x04\xbd\x22\xa5\x10\xf9\x27\x74\x27\xdf\x62\xea\x0c\xfb\xa1\x5d\x24\xa5\xa4\x4e\x3a\x1a\x6c\x19\xc4\xcd\xe2\x56\x6d\xf3\xf4\x0b\x9c\xdd\x4b\xb4\xfd\x20\xa3\x89\x5e\x17\x87\x80\x44\xda\xc6\x54\x37\x3b\x98\x12\x89\xac\x3e\xcd\x4a\xc7\x55\xc5\xa3\x72\xa7\x03\x72\x5d\x0d\x7c\x82\x4a\xe6\x2a\xf1\xcf\x58\x00\xb0\x81\x0d\x00\xbf\x3f\x11\x24\x00\x00\xc0\xc1\x08\x84\x83\xab\x1d\xfc\xfb\xa9\xa8\x05\x19\x1b\x32\xa8\x53\xf8\xcf\x9a\xcf\xcd\x4d\xe7\xbc\xa3\x59\x2c\x96\x0c\x64\xb3\x0c\x69\xc7\x8a\xb1\xdb\x1b\xeb\xa7\xfe\x90\x5b\xe4\xa4\xed\x19\xf7\xb6\x75\x76\x0d\x2f\x26\xda\xa2\x87\xe4\xa0\xa0\xbf\xb0\xa4\xa9\xa4\xa5\x82\x5f\x76\x3c\x7b\x29\x4a\xca\x07\xcf\x9a\x63\xed\x39\x7a\x35\x22\x09\x7d\x75\x85\xc0\xfa\x65\xf7\x9d\xc2\x6e\x85\xbd\x6f\x55\xfb\xab\x87\x15\x4e\x8c\xb1\x36\x38\xa3\x74\xf9\xb7\xb1\x30\x48\x3e\x09\xb5\xb8\x89\x9b\x3e\xa6\x53\x8e\xa1\x3e\xec\xab\x61\xfd\x2b\x3c\x44\xbc\x2e\x06\xd1\x38\xce\xa3\x2a\x06\x9a\x0a\x15\xe6\xa5\x2d\xfe\x8d\x0c\xc9\x7c\x15\xfd\x29\x23\xc3\xcf\xd1\x9d\x0d\x35\x20\xfe\x21\xdb\x6a\xf2\xea\x38\x96\x0f\x31\x7d\x45\x86\x86\x86\x8b\x50\xcf\x73\xae\x1b\x2d\x32\x2b\x60\x01\x43\x5a\x66\x39\x69\x49\x49\x01\x81\x80\x56\x19\x26\x7f\x4c\x53\xe5\x28\x7f\x47\xfb\xa3\xcb\x56\xe1\x81\xc7\xab\x1e\x2e\x11\x07\x87\x90\x0a\x43\xdf\xc7\x17\x92\xa9\x64\x94\x90\xe7\xf9\xa3\xe0\x8a\x0d\x94\xb2\x46\xb9\x9c\x46\x5a\xb3\x51\x59\x0d\xa6\xf9\xb2\x75\x2f\xeb\x45\x05\x3d\x05\xb6\xa6\xc6\xba\xb4\x6b\x02\x9a\x29\xcc\xc7\xa4\x51\xb6\x81\xbf\x95\xe6\x75\xeb\x84\x78\x0c\x63\xc4\x67\x4e\xfa\xa6\xf7\x8f\xda\x8a\x96\x53\xa2\xf7\xac\xda\x5b\x83\x19\x75\xea\x04\x9a\x60\x06\xc7\x69\x3a\x5e\x01\xc1\xd5\xcc\x9d\xd4\x56\x1f\xd2\x08\x64\xf1\x9a\x72\x2d\x22\x8b\xe5\x57\xd0\x65\x22\xf4\x5f\x87\x3f\xa2\xdd\x43\x44\x88\x8f\xe8\x3a\xf8\x57\x6f\xe6\x6e\xcc\xcc\xc7\x51\x25\x30\x6b\x30\x75\x30\xce\xd7\xd7\x4d\x32\x9b\xcf\x31\x3d\x75\xaf\x9e\x18\x9a\x2f\xf3\xb2\x7e\x39\x46\xbf\xea\x82\x51\x3a\xbe\xdd\xca\xe9\x19\x3e\xd3\xf5\x78\x91\xa7\x75\xc2\x54\xfe\xd8\x48\xb2\xb9\x9f\x77\xb4\x37\x37\xb8\x43\x89\x23\x52\x56\xa2\x32\x64\x64\x46\x3b\xd4\xeb\x6c\xf1\x10\x14\x6f\xaa\xb6\x2c\xf8\x09\xb7\x75\xd8\xaf\x2a\x80\xf5\x16\x76\x62\x0d\xbd\x13\x95\x68\x39\xc5\xf5\xda\xae\x2b\x22\xe3\xf2\x29\x1c\x3e\x54\x68\xde\x55\xc9\x67\x67\x1a\x3e\x4c\x6e\x3e\x82\x9a\xad\xb5\x1a\x7a\xca\x64\x03\xc8\x5a\xf3\xa5\x1a\xad\xbe\x43\x05\x85\xaf\xac\xae\x0c\x8d\x6f\x2b\xf7\x33\x90\xf6\xb1\x14\xe4\x16\x9b\x52\xc9\x77\xd5\x88\x08\xf7\xad\xaf\xf2\xc1\xb8\xa6\x60\x3b\x62\x09\x98\xc6\xe6\xf9\x77\x18\x72\x5d\x86\x99\xbb\x6e\x02\x02\x36\xaa\x12\x3c\xd8\x17\xec\x21\x9e\x51\x7c\xdc\x17\xf6\x59\xc5\x74\xb0\xfb\x14\xb3\x34\xd6\xc5\xd1\xf6\x52\xe2\x69\xab\x5a\x2d\x72\xc9\x89\x19\x63\x2b\xed\x87\xf5\x9d\x3e\x42\x39\x78\xc7\xe6\x31\xf7\x38\x85\xe7\xab\x12\x96\x21\x10\x4e\x71\x16\xba\x1a\x6e\xac\xa7\x58\xea\x0a\xfe\x39\x88\x41\x52\xd1\xae\xb3\x39\x3e\xaa\x57\x55\x4f\xbc\xb7\xee\x69\xb6\x89\x92\xde\x7a\x02\xfd\x64\x6b\x2b\x9d\xc6\x27\x53\x88\x48\x89\xb4\x08\xe6\x09\xd7\x72\x94\x09\xcd\x18\x52\x5f\xa3\xf5\xec\x40\xe1\xd0\x04\xa4\x08\x4d\x94\x0b\x06\x87\xb0\x0f\xeb\xf3\xbd\xd9\x78\x1d\x77\xe1\x6b\x89\xd2\x12\x8d\x82\x62\xfb\xf5\x24\xb2\xe1\x4c\xc1\x71\xa9\xbf\xa1\xf0\x3c\xd0\x2a\x58\xc7\xd8\x42\xfb\x31\x3a\x53\xf4\x1a\x03\xf3\x59\x6c\x9d\x62\xbc\x61\x9b\x6a\xaa\xa8\x72\xf5\xfd\x5e\x20\x3a\x07\xa5\x95\xdc\x5d\xba\xa4\x0d\x52\x70\x37\xb0\x13\xdd\xdd\xde\xcc\x4b\x79\x61\xe3\x2d\xc3\x52\x37\xf2\xd1\x71\xe7\xde\x8b\x00\xcf\xbb\x2a\x29\x50\xc2\x2d\xbb\x5b\x36\x92\x43\x58\x3a\x38\x19\xfe\x37\x92\xad\x3e\xec\xf1\x66\xfb\x78\x9d\x40\x5b\x44\xac\x68\x13\x2f\xe6\x52\xc4\x92\x35\xf5\x22\x9b\x84\x61\xe9\x9f\x06\xb6\x71\x45\x45\x69\x88\x96\xfd\x32\x65\x2d\x28\xa1\xef\x73\x17\x1f\x90\xbf\x54\xc8\xc6\x13\x73\xba\xb0\x8a\xae\xbc\x68\x47\x93\x96\x4b\x18\xbb\xa5\x55\x80\xd2\x29\x37\xdf\x3e\x8f\xe2\x7d\x7e\xf4\x7e\x97\xe8\xa2\x8d\x8d\x18\x4f\x76\x89\xe1\x46\xad\xa9\xe0\xd5\x94\x2f\xca\xc6\xd7\xae\x31\x56\xca\x17\xde\x23\x0e\x75\xfe\x0c\x14\xce\xf4\xb5\x1f\x93\x8e\x55\x0b\x6f\xb2\xb9\xa6\xba\xda\xa8\x38\x56\x5e\xb4\xc8\x34\x15\x9b\x50\x6f\xe3\xa4\x95\xd5\xc6\x57\x42\xdd\xcc\x9e\x81\xd2\xba\xe4\x46\x0d\x42\xec\xe3\x6e\x3f\xe1\x24\xe8\xd8\x77\xc4\x4e\x52\x7e\xc3\x7d\x70\x24\x30\x26\x33\x37\xe2\x6c\xad\x78\x58\xc0\xfc\xe1\x95\x6f\x2d\xa7\x52\x5c\x06\x0a\x14\x8a\x8a\xbf\x89\xd8\xdc\xc6\x62\x0a\x43\x2e\x4b\xea\x14\x67\xcd\xe9\x4c\x7d\x94\xdb\x71\xeb\x1c\x7a\x94\xef\x92\x2e\x82\x63\xb8\xec\x6c\x36\xc9\x35\xcf\x1c\x49\xc7\x5d\x0a\xa3\x7f\x85\x3f\xe2\x76\xf2\xa5\x42\xd2\xee\x76\x4a\x64\x67\xf9\x8b\x6f\x12\x95\x18\xda\xce\xfb\xe4\x5e\xf8\x37\x13\xb8\xc9\x48\xc5\x38\xba\x33\x7a\x2a\xb6\x04\x4d\x0d\xa5\x23\xf0\xdd\x8d\xd7\x56\xad\x19\x88\xeb\x5d\x3e\x83\x9c\x77\xa9\x4c\x76\x75\x6c\xb3\x18\xf9\xee\x91\x4e\x70\xae\x16\x7b\x8c\xa7\xeb\xba\x67\x5f\x18\xf1\x78\x68\x36\x97\xf8\xf3\xdc\x08\xf6\x0a\x50\x94\xcf\x9e\x44\x89\x8e\x1d\xf7\x45\xdc\x31\xc7\x16\x7b\x7d\x23\xcd\x3d\x7d\xf4\x63\xa8\xfe\x2e\xad\x0e\xaa\x2c\xec\xf0\x4d\xa1\xb5\x22\xaf\xca\x91\x86\x92\x8d\x71\x23\x1f\x9f\x1a\x45\xb9\xed\xb9\x8d\xa1\x9a\x96\xb4\xca\x97\x3b\x5e\x21\x33\x77\x0b\x56\xd4\x64\x8b\x6a\xd5\xb6\xb1\x49\xfc\x5f\x0c\x23\x95\xfb\x27\x5a\xce\xcb\x19\x06\xb6\x7c\x1c\x49\xfd\xa8\x4e\xfa\x8e\xb8\xc7\xc5\x83\x31\xcd\xfc\xc8\x87\x88\xe6\x68\xc3\xca\x39\x1d\xa1\x0b\x08\x8b\x08\xd5\x62\x76\xfe\x08\x75\x4a\x66\x5b\x97\x7e\xb3\x58\x28\xd2\xc2\xce\x37\x69\xb6\xfb\xdc\x79\xc5\x6f\xad\x48\x1f\x32\xfe\xbe\x95\xd6\xdf\x8d\xc2\x9f\xd1\xa7\x38\x85\x0a\x8f\x8e\xc8\xdc\x7c\x50\x08\x47\x60\xf5\xc6\x46\xd0\x47\xc5\x73\xbc\xc0\x30\x84\x1f\xfe\x82\x0d\xbb\x7e\x71\x52\x31\xbc\x68\xbf\x5f\x31\x36\x59\xfb\x51\xe1\xd9\x2e\xc1\xcd\xaf\xcf\x32\x48\xb1\xcd\x51\xfa\x70\x81\xcf\x05\xda\xb9\x1b\xab\x42\x05\x8e\x01\xc2\x83\x56\xca\xb6\x0b\xb5\xce\xc6\x24\x0a\xd3\x65\x64\x47\x92\x9a\xe0\xec\x0f\x9d\xfa\x8f\x50\xd8\x37\xd9\xe9\x9d\x4d\x0c\x83\x77\x85\xaf\x2d\x87\x58\x66\x57\xd4\xbd\xcb\xf8\xe3\x4b\x61\xa5\x11\xea\x6b\xea\x52\xa6\xaa\x87\xf0\xb2\x75\x19\xe9\x50\xce\xc5\x01\xb6\x61\x9f\xdc\x29\x8e\x9e\xfd\xc8\x74\xd5\x6c\xa4\x65\x3b\x50\xf5\x7c\xb5\x07\x3c\xdf\xcc\x50\xf7\xcd\xaa\xf9\xda\x99\x1d\xcf\xd7\xa7\x8b\x51\x0b\xc8\x9b\xc7\x51\x8c\x3a\x48\x52\xa3\x3c\xa2\xd6\x9c\x2b\x52\xdc\xc8\x7c\xee\x05\x7b\x15\x05\x72\xbb\x59\x40\x42\xd1\xeb\xf1\x81\x6f\x3c\x09\xab\x3a\xcd\xf9\x50\xfc\xdd\x2e\x11\x92\x5b\xd4\xa8\x0c\xe8\x90\xa8\x37\x92\xcd\xec\x19\x4d\x58\xde\x68\xdc\x9d\x3a\xa4\x38\xff\x10\x4f\xca\x5e\xe6\x2c\x28\x9e\xdc\x85\xbc\xe7\x8f\x44\x92\x20\x8d\x0c\x8b\xaa\x90\x65\x43\x5c\xf8\x18\xbd\x8c\x41\x34\x13\x4d\xc5\x17\x0f\x72\x9b\xa9\x69\xfc\x84\xb0\x4f\xa3\x53\xe5\xa8\x05\x70\x79\xce\x8d\x51\x2a\x17\xa5\x62\x2e\x3d\x13\x19\x34\x7a\x48\xbf\x72\x84\xfb\x71\xd0\x5c\x69\x98\x2c\x57\x0b\x17\xe3\xd3\x67\x99\xeb\xcf\xa1\x75\xce\x7f\xc8\xb9\x61\x48\x27\x4d\x78\xda\xbf\xec\x4c\x2b\x8d\xe7\xd9\x05\x50\x7b\xbd\xcb\xcf\x74\x20\x39\x33\xa0\x8c\xb5\x9f\xed\xd9\x19\x3d\x08\x1a\x34\x7c\x60\x00\x7f\xc2\x49\x82\x4d\xf5\x6d\xe2\xa4\xe5\x8c\xa0\x3a\xff\x3a\x11\x5f\xdf\x1b\xa8\x2c\xf9\x41\xb2\x92\xf7\x88\x93\xf4\x37\x43\x75\x8d\x92\xb9\xdb\x6e\xed\x2f\xdd\xf9\xe0\x7f\xf4\x87\xc6\x50\x05\x0f\x6e\x73\x62\xc5\x1f\xf5\xa0\xa5\xbb\xdc\x36\x6d\x35\xe8\x2e\x1b\x12\xb3\x58\xe8\x52\x66\xfd\x78\x4d\x66\x0e\x68\x0e\x30\x01\xdf\x8b\xd9\x2b\x92\x0c\xbe\x24\x3c\x00\x88\xe0\xfb\xb3\x98\xfd\xa9\x95\x7e\x2c\x66\x8c\x00\x00\x28\x41\x5c\x6d\x1d\xec\xdc\x61\xff\x51\x49\x70\x61\x01\x4b\x6b\x6b\xb0\x33\x18\x66\x89\x80\xc0\x04\x7e\x8f\x64\xf8\x05\x12\x8e\xb0\x44\xb8\xc3\xad\x2c\xff\x5b\x1c\x14\x02\x75\x87\xba\x80\x5d\xdd\xff\x01\xc7\xf2\x0b\x9c\x83\x8b\xa5\x1d\x18\x2e\xa0\xe8\x80\x70\xb1\x84\xc2\xff\x01\x4c\xf7\x0b\xf0\xf7\xf1\xfe\x79\xaa\xbf\x42\xd9\x3a\x43\x2c\x11\xe0\x7f\x42\xfd\x6a\x49\xa1\x30\x88\x1d\x0c\x0c\xff\x1f\x96\xe6\x57\xe3\x21\x20\x10\xe7\xff\x7e\x41\xbf\xa3\xa0\x96\xae\x60\x67\x81\x7f\xd6\x31\x2e\x96\xae\x0e\xb6\x60\x38\x82\x1f\x66\x63\x3b\x90\x34\xe0\x1a\x22\x48\x11\xba\x25\x13\x06\x1e\x9c\x90\x17\xc0\xc5\x85\x71\xf5\x36\x38\x66\xbf\x15\x0b\x37\xc2\x15\xd8\xcd\xa0\x9c\x69\x41\x26\x3d\x7a\x50\x50\xb2\x96\x3d\xb2\x50\xae\x2a\x32\x8b\xac\x79\x8e\xb1\x64\xec\x89\xae\x2a\x6e\xd3\x17\xb5\xe6\xe6\xe8\xbd\x67\x5f\xbb\xca\x2b\xd5\xc4\xdf\x50\x38\x25\x35\x39\x51\xc7\xcb\x92\xc1\xb4\xe3\x18\x52\x98\xcd\xd3\xe8\xe9\xca\x23\x69\x9e\x1a\xc6\x77\x87\xc4\x79\xa8\x3a\x8f\xcb\x2b\x63\x4f\x46\xe7\xcd\x8d\xb6\x51\x09\xb7\x7b\x24\xef\x6a\x90\x04\x84\xcc\xb1\x8a\x0e\x07\x5f\xfd\x18\x24\x07\xe7\x09\x6d\x0d\x1f\x20\xd6\x73\xe2\xbd\x8f\x58\x65\xa3\xa7\xa5\x5e\xe6\x54\x11\x59\xd6\x3d\x6c\x8e\xa8\x63\x4d\x08\x92\x8f\x25\xd4\xec\x0d\x45\x84\x30\x36\x6a\x6b\x74\x89\xa4\xee\x3d\x90\x6c\xcb\xc5\xcc\x77\x5b\x34\x0f\x87\xe4\xc9\x58\x6c\xf3\x22\x87\x26\x66\x22\x49\x50\x8b\x66\x5f\x9a\x2b\x37\x4d\x42\x66\xfd\x52\xec\x7d\x36\xad\x7d\xdc\x87\xbb\x76\x66\x6f\x26\x5b\x64\x0a\x6f\xea\xac\x35\x49\x7b\x5f\x2e\x5f\xf0\x0f\x5d\xd2\xcf\x57\xd4\x78\x28\x3b\xcb\x0f\xe6\xdb\x9d\xc4\x2f\xd7\xec\xb2\x18\xa8\x55\x7c\x7f\x73\x6a\x8e\xec\xd1\xb8\x58\x00\x10\xf2\x8f\x32\x90\xe8\xbb\x0c\x44\x78\x3b\x83\xff\x23\x02\x31\x20\xe3\xb8\x29\x71\x5a\xff\x59\x73\xcb\xe2\x95\x96\xf7\x89\x1b\xe9\xfd\x13\x03\x05\xe4\xb8\xb4\x7c\xf1\x88\xa0\xac\x7d\xcb\x6c\x22\x50\xde\x6d\x57\xae\x87\xac\x2f\x5f\x67\x99\xb7\x46\x50\x73\x3f\xa8\xb2\xe1\x4d\xaf\x69\x8a\x76\x27\x62\x17\x1b\x9a\x6b\xf0\x5b\x6e\x7f\x3e\x73\xe1\x75\x7b\x75\x1e\x97\x58\x5d\xc2\x97\x28\x11\xcc\x78\x5b\x50\xc3\x5b\x75\xe2\x82\xfb\x1b\x66\x61\xb6\xbd\xfb\x4b\xb7\x67\x80\x77\xdf\x5e\x66\x81\xd7\xf3\x9d\x1a\x7e\x21\xbd\x5c\x7d\xce\xa7\x8b\x57\x6e\x89\x34\xb8\x44\x66\xee\x15\xe5\xe8\x96\x4c\xaa\x5e\xad\x10\x74\x52\x7d\x66\xb2\x91\x6f\xc0\x08\xc9\x5b\xf2\xb4\x58\xde\x50\x49\xfd\x98\x7a\x09\x95\xb8\x4d\xee\xd4\xdd\x84\x3c\x7c\x10\x4e\x4d\x64\x4e\x20\xe7\xb7\x6c\xcb\x48\x6c\x80\x1c\xbc\x27\xfe\x5c\xcb\xa6\x18\x29\xb6\xb0\x18\xba\xc2\x7e\x05\x49\xd1\x1a\x48\x8e\x76\xeb\xd3\x8f\xbf\x2f\x8b\x5b\x6b\x61\x8c\x64\x4f\x30\x0e\xbf\x7f\xbb\x9e\x67\xda\xf1\x6e\xe6\x90\x71\xb6\xaf\x53\xbc\xa3\x2a\x8c\xbf\xb7\xa1\x5a\xac\xe2\x6e\xbd\x72\x49\xda\xf6\xaa\xd0\x09\xa0\xbc\x37\x1e\x71\x54\x66\x77\x85\x89\xff\x4a\x56\x67\xc2\x92\xdb\x19\x77\x1e\xb7\x9c\xd4\x72\x67\x4b\xac\xc5\xa0\x13\x1e\x9d\xd2\x48\x59\xf3\x37\x23\x4b\x05\x51\xc2\x07\x22\x2e\x53\x49\x5f\xae\x97\x7d\x40\x9a\x91\xd6\x5b\xc2\x44\x02\x25\x9b\xdf\x3e\xb9\xe6\xc0\xc7\xbc\x37\x77\x63\x4f\x81\x22\x4c\x65\x3d\x2f\xd9\x49\x2c\x7c\xae\x3b\x40\x39\x83\x96\x47\xf0\x2e\xec\xf3\x2b\x7e\x4f\xa3\x7e\xd2\x67\x83\x8a\x49\x6b\x76\x9b\x43\x29\x16\x69\xe7\x9f\xf2\x1e\x5e\xcb\x6c\x8a\xc3\x29\xff\x43\xb7\x0c\x8e\x4b\x59\xad\x37\x2c\x78\xda\x88\xb8\xd5\xef\x41\xe3\x95\xbb\x72\x3a\x3c\xfe\xe9\x6c\x75\xfd\x15\xfe\xc3\x67\xee\xe3\x42\x0c\x5f\x9f\x29\xf8\x79\xc8\x92\x75\xc0\x0b\x42\x9f\x64\x6c\x52\x65\x4c\x3e\xe2\x6f\x1b\xf1\x9e\x08\xb7\x7a\xc5\xcc\xca\xd1\xf1\x30\x3c\x54\x61\x66\xc7\x71\x7d\x60\x93\x46\xe4\x91\x7d\xf9\xa7\x8c\xfa\x08\x77\x43\x16\x03\xbe\x62\x21\x09\x8d\x9b\x9a\xaf\x5f\xb0\xe7\xdb\xa4\x2b\x9b\x76\xa4\x97\x2e\x72\x39\xa4\xed\x09\x2e\x82\x71\x6b\xe5\x7a\x94\x7b\x5b\xf3\x44\x8c\x62\x64\x08\x5b\x7a\xfa\xb4\xe6\x5c\x2b\xf1\xb6\xfd\xca\x59\x64\xf0\xf9\x4a\xc5\xef\x9a\x1d\x56\x57\x2f\x80\xef\xce\x3d\xb1\xee\xf5\xc3\x8e\x99\xb2\x6d\x39\x0f\x80\x7c\x34\xc1\xdd\xfa\x4c\x94\x0e\x37\x62\x82\x27\xcf\x0b\x76\x70\xa7\x6c\xcd\x88\x1f\x7f\x22\x13\xc8\x98\x05\x8d\xeb\x6d\xb7\x68\xab\x87\xd3\x0e\x45\xbe\x67\x37\x0a\x2c\x43\x56\xb0\xef\xef\x53\x9f\xd1\x18\x98\x5c\x7c\x11\x12\xc3\x6e\xcb\xe3\xd4\xdb\x0b\xf7\xed\x67\x55\x8d\x57\xda\x95\x2f\x20\xc9\xe3\xf5\x21\xdc\x97\xa7\x9a\xef\x12\xd1\xec\x2b\x72\x27\xa7\xb3\xa3\x63\xf5\x8a\x32\x8a\xcb\xbf\xc1\x13\xa6\xb2\x89\x4b\x68\x35\x16\x2a\x20\x48\xe6\xf7\x02\x3c\xf5\xcc\x62\x8f\x61\xb8\x51\xd7\xaf\x14\x9a\x55\xd0\xbe\xe8\xe4\x7c\x22\xff\xb0\x8d\x63\x68\x74\x3e\x7b\xc5\x4e\xf3\x16\x86\x61\xfb\x9c\xbb\x4e\x36\x69\xb7\xc1\xf6\xe6\xad\xda\xd0\x71\xc7\x38\x54\xef\x20\xf2\xce\xa9\xbe\x1d\x38\x1c\xae\xaf\xd7\xe4\xeb\x9e\x58\xa6\xee\x1b\x54\x12\x5c\x10\xb4\x50\xdb\xda\x41\xc4\x20\x47\xfb\xf4\xad\xab\x61\x13\x5b\xca\x97\x6c\xc1\x7d\xea\x5e\xcb\x6b\x3b\xd3\x27\x9b\x03\xb2\x8d\xc4\xb4\x3e\x07\xa1\xb8\x41\xe8\xd4\xdd\xca\xc9\xaf\xdc\xfa\x8f\x06\xeb\xf1\x29\x68\x76\xef\xa7\xbb\xb6\x79\x15\x07\x4c\x71\x57\xc9\xd3\xdf\x0d\xb5\x28\xeb\xe4\x91\x91\xc3\xdd\x81\x65\xa5\xe6\x77\x0e\x07\x16\x5b\xbc\x26\xb7\x14\xeb\xc8\x7d\xa5\x4b\xa4\xe1\x69\x04\x55\x90\x83\x4f\xa3\xc4\xd6\xd7\x76\x6e\xec\x45\x7b\xf0\xc8\xda\x85\x8d\xdd\x31\x5c\xa3\x5f\x8f\xd4\x24\x16\xc6\xbd\x4b\xa6\x4c\x63\x59\x2d\xd0\x73\x35\xc5\xec\x50\xff\x1e\x6b\x88\x27\xb9\x8b\x12\xfa\x8c\xad\x84\x66\x5c\x8a\xb5\xf0\x5e\xb8\x77\xc7\x53\x7b\x03\xd6\xa2\xb7\x69\xaf\xa9\xe8\xda\x7a\xc9\xd9\x6e\xbc\x5c\x03\x93\x59\xd2\x8f\x69\x99\xb2\x98\xfa\x45\x67\x71\x0c\x52\x4c\x94\x29\xc0\xb6\xef\xaf\x8d\x64\xd4\x95\x81\xc7\xdf\x65\xa5\x9b\x68\x90\xb4\x08\x33\x49\xe5\x7f\xb6\x0d\xa5\x14\xdb\xa4\x17\x1b\x8d\x66\xb3\x26\x31\x4d\x1c\x92\xd7\x79\x94\x22\x6c\x4d\xfd\xfa\x49\xec\xa1\x60\x68\x3a\xfd\x3d\xcb\xdc\x47\x57\xd0\xf3\xf5\xcb\xb4\x78\xaf\x2c\x22\x28\xfd\xdb\x83\xe9\xb7\x54\xdd\xc3\x2b\x27\xd9\xc4\x41\xa1\xa0\xc2\x9b\x9c\x3b\xbb\x2d\x8e\x96\x88\x49\xa6\x03\xf8\x1c\xd7\x18\x89\xda\xb7\xd5\x71\x78\x98\x0f\x2e\xea\x9b\x84\x73\x0e\x76\x33\x9f\xe1\xc2\xae\x3c\x44\x83\xaf\x47\x17\xae\x71\xb2\x38\x9e\x43\x6c\x52\x31\xb1\x8c\x08\x14\x47\xb7\x75\xbd\xc1\xf9\xda\x10\x38\x62\xbd\xcb\x6d\x24\x76\xa0\xc5\x3a\x86\xa9\x98\xd7\xa7\x49\x4b\x8f\x0c\xa7\x06\xd3\x77\x76\xd0\x39\xde\xcf\x4e\x4c\xac\x69\x19\xe8\xc7\x94\xd8\xbe\x2c\xb9\x1e\xd4\xe8\x0e\xe0\x00\x44\x9d\x47\x3e\xbb\x4d\x7e\x64\x22\x57\xf8\x02\x1d\x38\x2a\x8d\x1b\x74\x1a\x7b\x47\xec\xb8\x4d\x1b\x39\x86\x2a\xc3\x2d\xb6\x52\x16\xf9\x9a\x2f\xd7\xd1\x53\x3a\x82\x1b\x8d\xe3\x82\xeb\x38\xfc\x57\x2d\x73\xe5\xc8\x1a\x47\xa4\x75\x06\x4a\x52\xbb\x02\x17\x69\x6a\x87\x33\x0f\x9b\xc5\xef\xbf\xce\x96\x1c\xe0\xf6\xea\x0e\x2b\xc7\xcb\x72\xac\x54\x96\x9f\xd1\xd5\x57\xcf\xea\xae\x30\x60\x24\xf2\xa7\xe3\x8a\x48\x50\xf8\xe8\xdf\xfe\x07\x11\xcd\xac\xb5\xc1\x0a\x31\x60\xea\x39\x4e\x7d\x4a\x84\x41\xe2\x95\x70\x29\xdd\x20\xbe\xe5\x1b\x41\xcc\xac\x42\x09\x0a\xe6\xcf\x2d\xcf\x19\x5c\x3c\x8f\x79\xb0\x64\xb0\x5b\x20\x41\x67\x5f\x31\xdd\xee\xa6\x97\x56\xfe\x28\x7b\xee\x26\xc9\x59\xcb\xd3\x80\xd9\x9d\x0d\x51\xdb\xb1\x5a\x7e\xc3\x1b\x58\xa6\x78\xac\xa5\xfb\x3e\xfe\xf5\xc2\xeb\xde\x77\x5a\x16\xee\xaf\xfa\x8a\x1b\xad\x25\xa5\xdc\x12\x2b\x6a\xa3\xa6\x64\xbe\x28\xbb\xca\xc4\x5f\xe5\x42\xd1\xaf\x1a\x98\x1e\x0b\xe4\xe1\x23\x12\xc7\x41\x83\xca\x77\x85\xc0\x4b\x93\xef\x59\x02\x24\xe8\x3c\xca\x9b\xee\x6b\x89\x67\xef\x99\x1e\xa7\xdb\xf6\x1d\xbb\x37\xc1\xef\xf8\x3a\x3e\xdb\x58\xc9\xda\xd1\x73\x81\x8e\x44\x97\x32\x0d\x80\x86\x77\x9f\x1c\x7f\x2a\x7a\x2e\xbd\xaa\xd4\x27\x84\x51\x19\xd7\xaf\x65\x31\x95\x34\xe7\x5e\x78\x8d\x0e\xe4\xa8\xca\x5b\xf9\x4a\x33\xd4\xce\xd9\x8a\x12\x6d\x6d\x5c\xbb\xc5\xbb\xb9\x69\xe9\xda\x95\x3e\xa0\x10\xf4\xbe\x45\xd7\x67\x35\xaa\x35\x3b\x0e\xed\x3b\x21\xea\x17\x7b\x7a\x8d\x81\x7c\xf4\x86\x1d\xb5\xaf\x80\x8a\x21\x5c\x4f\xf4\x78\x01\x36\x62\x5a\xda\xaf\xb4\xc9\x69\xa3\xed\x3b\x2b\xe8\x09\xf0\x04\xb5\x54\x82\xa7\x5a\xce\x32\x52\x5d\xca\x6d\x9f\x4b\xc3\xe1\xd0\xc4\x27\x4f\xd5\xf9\x3d\x0b\x99\x75\xea\x77\xa2\x64\xcd\x45\x50\x76\x6f\x97\xae\x91\xa5\x41\xcf\x5e\x64\xe1\x90\xda\xa6\x4a\x09\x1f\xdf\x4d\x65\xdf\x13\xf7\x7f\x41\xce\x37\xed\x23\x24\xdd\xe7\x1f\x84\x8d\xd9\xb9\xbe\x44\x27\xb4\xcb\xbf\x34\x71\xa7\x37\xe2\xb2\xf9\xfe\x9d\xcd\x1d\x90\x58\x3d\xbe\x5d\xee\x37\x5f\x25\x42\xbc\x18\xea\x56\x0b\xc7\x4f\xd7\x03\xf7\x01\x89\x49\xd7\xc9\x35\xe1\x11\x48\x30\x41\x06\x3c\xfa\x8d\x6b\xa2\xc7\x1a\x85\x4f\x76\x27\x87\x90\xb4\x77\x64\xe1\x4b\x63\x67\x72\x37\x6a\x4c\x20\xc9\xcb\x1a\xbb\x13\x3c\x5f\xcd\x8d\xc0\xae\x5e\xf1\x10\x49\xe7\xbe\x63\x2c\xbb\xb9\xcd\x91\x1a\xa7\x09\x1d\x5f\x68\xcc\x24\xde\x08\x77\xbd\xdd\x95\xaa\x46\x2c\x34\xbe\xcd\x13\xec\x02\xf2\x9e\x69\xd2\x78\x39\xd6\xa3\x3b\xf2\x27\x81\x6f\x1a\xb0\xd0\xf8\x5f\xe7\xf0\xee\xb1\xc4\x77\x5f\x67\x7d\x22\x2f\x7e\x47\x1e\xef\x7a\xf5\xb7\x4b\x82\x04\xb9\xf7\x11\x51\x6e\x61\x1c\x1b\x51\x8f\xb5\x0f\x6b\x9b\xd5\x9c\x08\x73\x65\xf9\xbf\x66\xf2\x9e\xfb\x92\xbc\xee\x64\x26\xc8\x88\xe8\xbe\xa6\xec\xf4\x8a\x1c\x67\x20\xe7\x39\x97\x71\xc1\x46\x5f\xc8\xdd\x22\x49\x95\x95\x22\x42\xbe\xa1\x27\xc8\xdc\xf3\x2c\x08\x9b\xed\xa7\xe4\x20\x9b\xcf\xf2\x9c\x66\x37\xd1\x8f\x05\x51\xb6\xbb\x24\x23\x32\xe3\x19\xbc\xc3\x03\x28\x5f\x7b\x7a\xa5\x6e\xa2\x55\xdd\x1c\x09\xc3\x54\x55\x23\x9e\xfe\xfa\xbb\x3e\x85\x47\x66\x8d\xb1\x35\x9b\x41\xc6\xe6\xd6\x21\x9a\x12\x6d\x12\xc1\xd2\xcd\x18\xb6\xc9\x15\x9c\x8f\x98\x00\x7e\xdd\x14\x75\xc1\x76\x51\x89\xb2\xeb\xbd\x41\x36\x27\xeb\x9d\x06\x55\x31\x0b\x9d\x33\xec\x74\xc1\x4d\xda\x20\x97\x54\xf2\x24\x66\x10\x0d\x90\x43\xd4\xc0\x7a\xb2\xc7\x76\xdd\x91\x3f\x59\x5e\x19\xb7\xe3\x7a\x74\xd4\xd4\xd3\xb6\xca\x77\x2b\xce\xba\x46\x21\x28\xe6\xba\x62\x82\xd1\x72\x5f\x84\x10\xf3\x16\xee\xfb\xc9\xe5\x13\xd1\xb1\xcb\x58\xb7\x09\x8a\x0a\xd5\x28\x69\x76\x69\x89\xf9\x45\xa5\x33\x54\xf5\xdb\x65\xaa\x83\xcd\xaf\x3b\x65\x5b\xe3\x0b\x7e\xb1\xb5\x31\x22\x32\xdc\xb8\xa0\xa0\x0b\x39\x34\xea\xb3\x64\x92\xc2\xd9\x95\xe9\x19\x38\x99\x84\x89\x56\x38\x87\x94\x47\x78\x70\xc2\xf4\x35\xe6\x54\x85\xf9\x5e\x85\xcf\x5f\xd5\xde\xc4\x24\x75\x0f\x32\x1d\x8d\x10\x84\x2b\x8a\xd3\xf9\x0f\x24\x75\xbe\x27\x20\xef\x3d\x08\x99\xa1\xd3\x18\x4f\xfa\xa2\x39\xc8\xed\x20\xc0\xab\x7b\x58\x01\x2a\x41\x09\x67\xf8\x38\xbc\xc3\x15\x15\x7f\xf6\xfa\x68\xd6\x33\xce\x5e\xcb\xcf\x7a\x8f\x73\x7b\xf6\xa0\x1b\x36\xeb\x6d\x6c\x51\x90\x5d\xd2\x1f\xd7\x13\xf1\xf6\xba\xcc\xb5\x11\x83\x55\xef\xc7\x20\xc5\x28\x1b\xde\xfc\x51\x5b\xfc\xda\x82\xe8\x0a\xad\x96\x01\x33\x49\xdf\x69\x4b\x56\x39\x86\x4b\xd7\xd3\xe9\x56\xac\xef\xaa\xc3\x7f\xfe\x50\x4e\x8e\x00\x00\x8e\xf8\xfe\x49\x75\x10\x03\x00\x60\x0d\x71\x45\x80\x5d\x11\xdf\x65\x47\xb3\xd1\x03\xc8\x94\x38\x85\xff\x6c\x05\x85\xa8\x05\x44\x98\x6f\x8a\x8f\x91\xcf\xc3\x3c\xdc\x0a\x9b\xd2\x22\x4d\xd6\xe2\x09\x0f\x5d\x54\x62\xd4\x83\x02\xd5\x87\xa9\xcd\x79\x97\xcd\x6a\xf7\xf3\xc0\x5d\xb0\x64\x9f\x3e\x0d\xca\xdd\x96\xa6\xd3\x4d\x2f\xcf\x02\xd8\xe5\x5c\xbc\x08\x44\xa7\x20\x2f\xb8\xe2\x0c\x2a\x7c\x20\xdb\x6e\x9d\xa0\xfb\xa2\xec\xec\x6e\x3d\x4b\x48\xe1\xa5\xe5\x79\x47\xf3\x3d\x3d\x86\x24\xae\xab\x6e\xe9\x54\xe5\x21\x46\x50\xd2\x56\xd9\x0f\x4b\x3c\x25\x85\x8a\x91\x29\xad\xc3\x8d\x91\x14\x94\xb8\x73\x07\x9b\xca\x3d\x24\x03\xa8\x61\x69\xfd\x8a\x73\x55\xce\x24\xf5\x76\xc6\x96\x92\x12\x75\x31\xc8\x6b\x78\x72\x2e\x8e\x13\xe3\x06\xf7\x4b\x7f\x67\x31\x32\xe9\x2c\xf3\x11\x82\xad\xcd\xcd\xf8\x14\xa9\xe2\x3c\x6c\x94\xd2\xfe\xf8\x42\xc7\xad\x99\x04\x46\xfc\x64\xde\xd8\xe3\x28\xb4\x02\x87\x80\x93\x24\x94\xc7\xa6\x1c\x6f\x5b\x44\x3f\xda\x18\x7a\x63\x57\xa1\x2f\x90\xa0\x87\x39\x71\xf5\x03\x4f\x6a\xcd\x07\xde\xd6\x64\x19\x4a\xa3\x07\x34\x6e\x99\x54\x34\xab\x12\xc2\xde\x2e\xe4\x6c\x6b\x79\x37\x7d\x11\x07\x48\x37\xd9\x47\x53\x16\x4d\x8d\x92\x63\x4d\x41\x8c\x02\x1f\x6f\x12\x60\xe2\xba\xf1\xaf\xa5\x77\x8e\x8e\x86\x90\x17\xf5\x58\x80\xfc\x62\x1e\x6b\x92\xb9\x40\x85\xe1\x1c\x29\x4f\x13\x3b\xd2\x95\xe8\xc5\xfd\xde\xe7\xb2\x26\x32\x5c\xb5\x7d\xc4\x27\x55\xfd\x81\xa3\x6c\x87\xca\xf7\xf5\xd9\xed\xe2\x51\x00\xea\x87\xdb\x2c\xff\x4e\x03\xdf\xc4\x14\xd7\x30\xd1\xf1\x86\x69\xe4\xfd\x81\x83\x42\x46\xd7\x4b\x79\x55\x5a\xfb\x9c\x38\x90\xa8\xc0\x1e\xe7\x8d\x6e\xe6\x67\xab\x44\x85\x71\x31\x32\xc4\x01\xaf\x70\x89\x37\x1d\xfb\x14\xcd\x09\x37\x99\x12\x0b\x28\x1e\x36\x7d\xf1\x30\x42\xf2\x6b\x11\x6f\x9e\x08\xa7\x08\x4a\xe9\xbf\x6d\xa7\x96\x32\xcc\xe6\x5c\x18\x5d\x7e\xff\x38\x51\xdb\xe0\x08\x86\xf4\x39\x00\x8b\xc8\x35\xb2\x9c\x76\xc3\x2d\x92\x2f\x65\x65\xea\x63\x46\xdc\xf9\x39\x5e\xc5\x7e\x60\xea\x2a\xf4\x0f\x4c\x24\x81\xe1\x52\x91\xac\x69\x53\x3f\x50\x3b\x7d\x95\x1e\xf5\xce\xda\xe1\xba\x73\x40\x24\xe3\xbb\x07\x42\x54\x9a\xec\x0e\xd9\x5a\x90\x2a\xf6\xa2\x70\xde\xfb\xef\x9d\xf1\x12\xe9\xae\xaa\xc6\xb0\x85\xb5\xab\x47\x6a\x0f\x76\xa3\xe5\xb8\x68\x0b\xae\x6c\xe6\xbf\x09\x9c\xfb\xc8\xeb\x72\xab\xc5\x7a\x44\x34\xc1\xe3\x36\x42\xcb\x2c\x5b\x38\x7a\x86\x83\x13\xfd\xda\x2d\x92\xfa\x4d\xbf\x49\x1b\xa6\xfa\xe3\x62\xdd\x10\x5e\x5e\xc5\xcb\x18\x4e\x8b\x8e\xba\x93\x81\xd9\x2c\xbb\xb6\x7e\xf3\x7a\x48\xe0\x49\xcb\x83\x1e\x97\x82\x6d\x5a\x82\x87\x8c\x49\xac\x6c\x42\x7a\xeb\xa7\x8d\xd8\x9f\xeb\x0c\x2b\x62\xf7\x52\x2e\x75\xdf\x89\xb6\xcd\xe0\xc1\x69\x4e\x98\xe3\x21\x6e\x92\xe0\xee\x94\x4b\x2a\xfe\xd0\x93\x06\xec\x92\x3b\x7e\x5b\xfd\xa8\x36\x3c\xfa\x39\xa8\xbd\xae\x7e\x49\xde\xfd\xe3\x15\xdc\x56\x6a\xd4\x2b\x59\x38\x04\xdb\x71\xb5\x72\x8b\x19\x8a\x83\xc5\x0d\xeb\x1f\xa2\xc0\x1d\x15\x32\xc9\xf7\xbd\xfd\xb2\xd9\xdd\x4c\x81\xcc\x8a\xb9\x0b\x23\xd6\xd0\x10\x36\xb9\xca\x75\x51\xba\x20\xc5\xa3\xb5\x6b\x2c\x21\x8b\x36\x22\xe2\x78\x88\x79\xd2\xde\x4a\x29\xd5\x53\x1a\xe1\x4b\xe5\x7a\x3d\x03\x72\xdf\x3f\x28\x91\xea\xf3\x1a\x13\x69\xad\x69\x75\x9c\x06\xe9\x41\xb1\x03\xa3\x91\x74\x39\x0b\x66\x48\xa4\xbc\x35\x62\xd0\x50\xa9\xea\x2d\xa5\x1d\xe5\xb5\xa6\x47\xc0\xf5\xa5\x06\xc8\x35\x05\x2a\xb6\x37\xe9\x54\xb7\xa9\x5a\xc5\x9c\x5d\xfb\x72\x02\xdb\x74\x0a\x50\xca\xde\x9b\x4e\xac\xc1\x3b\x58\xf5\xf4\x32\xb0\x67\xd5\x47\x5b\xb4\xfe\x6a\xf3\x8c\xb8\xb2\x31\x84\x95\x1d\x47\x44\xc7\x9f\x2e\x15\xe5\x68\xdc\xad\x68\xce\xaf\x7d\x99\xcb\x0b\x7c\xa6\xd3\x64\x78\x35\x03\xf3\x45\xce\x9b\xf2\xf6\x0e\x84\xb8\xda\xf7\xdd\x3b\x03\x60\x8f\x7e\x6f\xcc\x52\xcd\xe8\xd4\xb9\xab\x76\xb9\xa5\x9c\xfe\x32\x86\x31\xbc\x96\xe9\xed\x5a\x9c\xce\xce\x19\xdf\xc3\x37\x7e\xeb\xd4\x02\x90\xfb\x3b\x81\x87\x9e\x28\xaf\xd3\x57\xb8\x5a\x75\x66\xc3\x20\xaf\x13\x7f\xb7\x36\x36\x86\x4c\xbc\xd8\x2f\xea\xec\x6a\xf5\x82\xfe\x63\x47\x87\x9f\x4a\x7b\xee\x72\xcc\x15\xbd\xdc\xf5\x1b\xae\x36\x26\x34\xc0\xed\xe8\x96\x4f\x66\x30\xab\x64\xb7\xd8\x26\x25\x36\x5f\xde\xf6\xa9\x9c\x97\x7a\x33\x4b\xc7\x9b\x52\x5e\x71\xbb\x63\xe7\x96\xf5\x94\xc7\x95\x19\xdb\xae\xaf\x47\x8f\x6e\xb6\x8e\xcc\xde\xba\x33\x75\x7c\xaf\xfa\xe1\x98\xde\x13\x89\x63\x95\xb2\x8f\x18\xa1\x95\xfe\x83\x54\x2e\x8c\x0b\x61\x32\xd3\x58\xc1\x47\x81\xf2\xe2\x50\x24\x86\x83\x5b\x66\xbb\xc5\x2f\xf7\x0d\xd7\x41\x01\xe1\xcb\xb7\x67\x92\x64\x6b\xee\x65\x6d\x3b\x76\xd5\xce\xea\xd8\xb0\xf3\x83\xf5\xa8\x2b\x52\xa5\x3c\x88\x47\x50\x4c\xc9\x65\x76\xc1\xc1\x9d\xef\xe9\x85\xf5\x55\x89\xc9\x2d\x1c\x00\x78\x4a\xf9\x77\x3b\x00\xcb\xd5\xbd\x0e\x74\x05\x00\xbe\x3f\xdf\x6f\xa0\xfa\xf6\xee\x2e\x56\xae\x96\x0e\xce\x70\x01\xc4\x9f\x1f\xf9\xa1\xae\x76\x51\xba\xda\x6a\xa4\x44\x0c\xdf\xef\x3d\xa4\xea\xf7\x94\x1f\x02\x00\x50\x0b\x00\x58\x00\x01\x36\x00\x00\xed\xe7\xdd\xab\xdf\xb1\xba\x9a\xfa\x2a\x62\x62\x62\xd2\xd2\xd2\x0a\x0a\x0a\x9a\x9a\x9a\x7a\x7a\x7a\x20\x10\xc8\xca\xca\xca\xd9\xd9\x19\x06\x83\xf9\xf9\xf9\x85\x84\x84\x44\x47\x47\x27\x25\x25\x65\x66\x66\xe6\xe7\xe7\x97\x96\x96\x56\x57\x57\xbf\x79\xf3\xa6\xbd\xbd\xbd\xb7\xb7\xf7\xe3\xc7\x8f\x53\x53\x53\x5f\xbf\x7e\x5d\x5d\x5d\xdd\xdb\xdb\xbb\xb8\xb8\x00\x00\xe0\xf2\xf2\xd2\xc5\x2b\xf1\x0c\x00\xb0\xda\xd4\x95\x15\xf4\xbd\x26\x31\xa3\xd3\xc8\xe7\x82\x14\x78\x1f\x6c\xa2\xb0\x42\x02\xee\x71\x69\x9c\x5d\x08\x80\x12\xef\xa7\xd4\x5d\x6b\x7e\x35\xb1\x16\x83\x53\xf7\x65\xc9\x5e\x34\x63\x0b\xdd\x62\x6d\x5a\xfc\xf2\xaf\xb8\x2c\xe6\x88\x5d\xf5\xce\xec\x4e\x5a\x30\xf1\x0f\x10\x58\xf5\xc2\xd8\x19\x40\x9c\xea\xdf\x71\x3c\x2b\xaf\xe3\x5b\x3a\xd5\x47\xa5\xed\x26\xa2\x28\xd7\x6b\xcc\x98\x87\x17\x32\xa9\xaa\x3c\x8e\xc5\x3d\x5e\x4f\x81\xa2\xbd\x5b\x48\xc7\xd3\x04\xc0\x6b\x64\x67\x6a\xe7\x91\x96\x72\x45\x8d\x7e\xb7\xe7\x5e\x28\x9a\x1b\xaf\x2f\xa0\xc7\xd7\x25\xcc\xef\xaf\xa0\xa1\x98\x5e\x77\xe6\x31\x78\x49\xf6\x90\x35\xea\xe8\xc8\xa2\xb5\x7c\xab\x77\xe1\xf4\x70\xbd\xbb\xfe\x13\x5b\xf6\xf3\x01\x35\xad\xf3\xad\x51\x77\xf3\x8b\x0c\x76\xf7\x80\x4c\x9d\x66\x96\xb6\x34\xbd\x5e\x87\x32\x8e\x72\xb1\x94\x7c\x98\x13\xb7\x4b\x03\x33\x5b\xc3\xc8\x9c\x64\xa6\x4e\xf3\xa0\x03\x7d\x3b\x05\x5a\xe6\x70\x21\x2e\xb1\xe9\x58\x5c\xa0\x6c\xfa\x84\xf1\x84\x9b\xcc\x14\x69\x2c\x91\x7d\xe2\xbd\x7f\x52\xf8\xc2\x91\x60\x41\x22\x75\xea\xa6\x8e\x9a\x66\xb8\x3f\xe3\x89\x24\xd9\xdc\xca\x3e\xa1\xef\xb4\x19\xe1\xc4\xe4\xb9\x06\x81\xff\xa6\x79\xd5\x30\x59\x2e\xea\x7f\x6f\x5c\xcc\x8e\xdc\xc5\xd2\x92\xca\xbb\xbd\x05\x51\x79\xf9\xbd\x98\xa9\xab\x68\x2b\x97\x2b\x5a\x04\xff\xbe\xdc\xd1\x00\x00\xa0\xa5\xa2\xaf\xc0\xa7\xae\xad\x2a\xf0\xff\xcc\x0a\x2f\x17\xe7\xb2\x24\x05\xd7\x0e\x56\x0a\x95\x23\xc9\xb0\xae\x3a\xe1\x9a\xc7\x06\x5c\xda\xb4\x09\x3e\xc1\xfb\xc0\x08\x33\x23\x3b\x1d\x05\xc9\x03\xd8\xa9\x15\xaf\xb6\x89\x41\x82\x15\xb7\xb4\xc5\xbb\xad\xcb\xe3\xb4\x81\xcd\x0d\x67\xf7\xe0\xb4\x98\xee\xf0\x27\xdb\x9e\x8f\xb0\xea\x5a\x44\x26\xee\x6c\x9c\x6e\x1b\x04\x54\x4d\x7b\xd4\xba\x11\xc5\x86\x2b\xc3\x40\xfa\x8b\xd2\x9a\xf5\x2e\x1d\x30\xe5\x55\x03\x61\xf5\x3c\x58\x3c\x7b\x2d\x1a\xff\xed\x60\xfd\x27\xac\xc4\xb3\x29\x01\xb5\xc2\x29\x57\x5b\x8b\x81\x72\xfb\xf5\x39\x19\x37\x6b\x01\xce\xf8\x43\x7a\x69\x95\xc2\x53\x1e\x9c\x65\xe0\x23\x09\x07\xb8\x70\x7c\xb2\x86\xd5\x2e\xf4\x81\x43\x53\xc9\xe6\x4d\x7b\xeb\x5c\x7e\xcc\x0e\xec\x4e\x4e\x75\xad\xbb\x2e\xc2\x39\xcd\x6b\x65\x9d\xf9\x0a\x42\x75\x14\x41\xf8\x41\x90\x26\x58\x0c\x63\x7f\x2f\xb3\x72\xc1\x1a\xcf\x4f\x4e\xaa\xc8\x2a\x24\x8a\x83\x99\x31\x7e\x1e\x1c\x89\x9f\x81\xad\x4b\xb2\xa8\xf2\x01\x47\x79\x11\x34\xdc\xd7\xe6\x1b\x37\xb9\x2f\x89\x64\x2b\xa8\xf5\x12\x7e\x99\xec\x56\xd9\x5e\x3d\x87\xc9\xac\x14\x2c\x59\xa7\x8f\xcb\xf2\x57\xfc\x03\xd1\xcc\xa7\x83\xe4\xa7\x35\xcb\x36\x4e\xcf\x08\xbf\x5c\x18\xfd\xdc\x21\xd4\xc4\xa2\xff\xee\x96\xc2\x70\x22\xfe\x06\x4b\x24\x5d\x91\x0f\x85\x52\x73\xb1\x73\x67\x6b\x51\xcb\x39\xee\xf7\x04\x70\x7e\x23\xf8\x39\x3f\x16\x00\x70\xe0\x7c\x4f\x00\x58\x57\xa8\x81\xdf\x77\x4f\x7f\x8e\x3f\x7b\xa9\x7f\xa2\xfe\xdc\xa8\x1f\xdb\x65\x3f\xa3\xb4\x7e\x68\x9a\xfe\x1d\xf5\xa3\x2f\x49\xf2\x13\xca\xfc\xca\xcf\x8d\xb5\xbf\xcf\xf2\xef\x26\xdb\x5f\x71\x9b\xf0\x7f\x72\x31\x7f\xcf\xc4\xf0\x13\x93\xed\x2f\x98\x7e\x74\x35\xff\x2d\xcf\x8b\x5f\xf0\xfc\xe8\x72\xfe\x9e\x87\xe5\x27\x9e\xb1\x5f\xf0\xfc\x7f\xae\xe7\xef\xc9\xe8\x7e\x22\xa3\x22\xfa\x27\x17\xf4\xdf\xb2\xa8\xff\x82\xe5\x2f\x57\xf4\xdf\x6e\x59\xc0\x2f\x58\x7e\x76\x49\xff\xed\x7c\x1a\x7e\xc1\xf4\x97\x6b\xfa\x6f\x37\x6c\xeb\x37\x2c\x7f\xba\xa8\x3f\x1f\xe2\x1f\x2d\xc2\x9f\x0f\x31\x27\xf1\xcf\xae\xea\xdf\x91\x3f\xca\x7c\xa2\x9f\x90\xb6\x24\x3f\x1a\x8a\x7f\xc7\xfd\x58\xbf\x89\x7f\xc2\x7d\xa6\xfe\xe9\x4a\xf0\xf7\xbf\xfc\xf7\xf2\xfe\x57\x48\xd3\xff\xbe\xd8\xff\x7d\xf8\x1f\xb3\x07\xcd\x4f\x2c\x7d\xd7\x7f\x93\xa2\x75\x35\x70\xf1\xbe\xff\xe0\x2a\x70\x15\x00\xe3\x00\x80\x0c\xd3\xf7\x6f\xff\x27\x00\x00\xff\xff\xae\xb0\x15\x1b\xb7\x21\x00\x00")
+
+func testAssetsTest_templateOdtBytes() ([]byte, error) {
+ return bindataRead(
+ _testAssetsTest_templateOdt,
+ "test/assets/test_template.odt",
+ )
+}
+
+func testAssetsTest_templateOdt() (*asset, error) {
+ bytes, err := testAssetsTest_templateOdtBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "test/assets/test_template.odt", size: 8631, mode: os.FileMode(420), modTime: time.Unix(1568821589, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+// Asset loads and returns the asset for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func Asset(name string) ([]byte, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
+ }
+ return a.bytes, nil
+ }
+ return nil, fmt.Errorf("Asset %s not found", name)
+}
+
+// MustAsset is like Asset but panics when Asset would return an error.
+// It simplifies safe initialization of global variables.
+func MustAsset(name string) []byte {
+ a, err := Asset(name)
+ if err != nil {
+ panic("asset: Asset(" + name + "): " + err.Error())
+ }
+
+ return a
+}
+
+// AssetInfo loads and returns the asset info for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func AssetInfo(name string) (os.FileInfo, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
+ }
+ return a.info, nil
+ }
+ return nil, fmt.Errorf("AssetInfo %s not found", name)
+}
+
+// AssetNames returns the names of the assets.
+func AssetNames() []string {
+ names := make([]string, 0, len(_bindata))
+ for name := range _bindata {
+ names = append(names, name)
+ }
+ return names
+}
+
+// _bindata is a table, holding each asset generator, mapped to its name.
+var _bindata = map[string]func() (*asset, error){
+ "test/assets/test_expected.pdf": testAssetsTest_expectedPdf,
+ "test/assets/test_template.odt": testAssetsTest_templateOdt,
+}
+
+// AssetDir returns the file names below a certain
+// directory embedded in the file by go-bindata.
+// For example if you run go-bindata on data/... and data contains the
+// following hierarchy:
+// data/
+// foo.txt
+// img/
+// a.png
+// b.png
+// then AssetDir("data") would return []string{"foo.txt", "img"}
+// AssetDir("data/img") would return []string{"a.png", "b.png"}
+// AssetDir("foo.txt") and AssetDir("notexist") would return an error
+// AssetDir("") will return []string{"data"}.
+func AssetDir(name string) ([]string, error) {
+ node := _bintree
+ if len(name) != 0 {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ pathList := strings.Split(cannonicalName, "/")
+ for _, p := range pathList {
+ node = node.Children[p]
+ if node == nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ }
+ }
+ if node.Func != nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ rv := make([]string, 0, len(node.Children))
+ for childName := range node.Children {
+ rv = append(rv, childName)
+ }
+ return rv, nil
+}
+
+type bintree struct {
+ Func func() (*asset, error)
+ Children map[string]*bintree
+}
+var _bintree = &bintree{nil, map[string]*bintree{
+ "test": &bintree{nil, map[string]*bintree{
+ "assets": &bintree{nil, map[string]*bintree{
+ "test_expected.pdf": &bintree{testAssetsTest_expectedPdf, map[string]*bintree{}},
+ "test_template.odt": &bintree{testAssetsTest_templateOdt, map[string]*bintree{}},
+ }},
+ }},
+}}
+
+// RestoreAsset restores an asset under the given directory
+func RestoreAsset(dir, name string) error {
+ data, err := Asset(name)
+ if err != nil {
+ return err
+ }
+ info, err := AssetInfo(name)
+ if err != nil {
+ return err
+ }
+ err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
+ if err != nil {
+ return err
+ }
+ err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// RestoreAssets restores an asset under the given directory recursively
+func RestoreAssets(dir, name string) error {
+ children, err := AssetDir(name)
+ // File
+ if err != nil {
+ return RestoreAsset(dir, name)
+ }
+ // Dir
+ for _, child := range children {
+ err = RestoreAssets(dir, filepath.Join(name, child))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func _filePath(dir, name string) string {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
+}
+
diff --git a/test/common_test.go b/test/common_test.go
new file mode 100644
index 000000000..4eb9a9ecd
--- /dev/null
+++ b/test/common_test.go
@@ -0,0 +1,25 @@
+package test
+
+import (
+ "encoding/json"
+ "regexp"
+)
+
+func toMap(i interface{}) map[string]interface{} {
+ var r map[string]interface{}
+ j, _ := json.Marshal(i)
+ json.Unmarshal(j, &r)
+ return r
+}
+
+func removeUpdatedField(i map[string]interface{}) map[string]interface{} {
+ delete(i, "updated")
+ return i
+}
+
+// Removes the variable data in the PDF like for example the creation date or the checksum
+var cleanPDFRegexp = regexp.MustCompile(`(?s)<<\/Creator.+?>>|<<\/Size.+?>>`)
+
+func cleanPDF(src []byte) []byte {
+ return cleanPDFRegexp.ReplaceAll(src, []byte{})
+}
diff --git a/test/common_test.go-e b/test/common_test.go-e
new file mode 100644
index 000000000..4eb9a9ecd
--- /dev/null
+++ b/test/common_test.go-e
@@ -0,0 +1,25 @@
+package test
+
+import (
+ "encoding/json"
+ "regexp"
+)
+
+func toMap(i interface{}) map[string]interface{} {
+ var r map[string]interface{}
+ j, _ := json.Marshal(i)
+ json.Unmarshal(j, &r)
+ return r
+}
+
+func removeUpdatedField(i map[string]interface{}) map[string]interface{} {
+ delete(i, "updated")
+ return i
+}
+
+// Removes the variable data in the PDF like for example the creation date or the checksum
+var cleanPDFRegexp = regexp.MustCompile(`(?s)<<\/Creator.+?>>|<<\/Size.+?>>`)
+
+func cleanPDF(src []byte) []byte {
+ return cleanPDFRegexp.ReplaceAll(src, []byte{})
+}
diff --git a/test/form_test.go b/test/form_test.go
new file mode 100644
index 000000000..b2f698d85
--- /dev/null
+++ b/test/form_test.go
@@ -0,0 +1,115 @@
+package test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+)
+
+type form struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestForm(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ f1 := createSimpleForm(s, u, "form1-"+s.id, "test-"+s.id)
+ f2 := createForm(s, u, "form2-"+s.id)
+
+ deleteForm(s, f1.ID, false)
+ deleteForm(s, f2.ID, true)
+}
+
+func createForm(s *session, u *user, name string) *form {
+ now := time.Now()
+ f := &form{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/form/update").WithJSON(f).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/form/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(f.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == f.Name {
+ f.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return f
+}
+
+func createSimpleForm(s *session, u *user, name, fieldName string) *form {
+ f := createForm(s, u, name)
+ f.Data = simpleFormData(fieldName)
+ return updateForm(s, f)
+}
+
+func simpleFormData(fieldName string) map[string]interface{} {
+ j := fmt.Sprintf(`{
+ "formSrc": {
+ "components": {
+ "5zvr98w21yynozx60nhmc": {
+ "_compId": "HC2",
+ "_order": 0,
+ "autocomplete": "on",
+ "help": "test-help",
+ "label": "test-label",
+ "name": "%s",
+ "placeholder": "test-placeholder",
+ "validate": {
+ "required": true
+ }
+ }
+ },
+ "v": 2
+ }
+ }`, fieldName)
+
+ var result map[string]interface{}
+
+ err := json.Unmarshal([]byte(j), &result)
+ if err != nil {
+ return nil
+ }
+
+ return result
+}
+
+func updateForm(s *session, f *form) *form {
+ s.e.POST("/api/admin/form/update").WithQuery("id", f.ID).WithJSON(f).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(f))
+ s.e.GET("/api/admin/form/{id}").WithPath("id", f.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return f
+}
+
+func deleteForm(s *session, id string, expectEmptyList bool) {
+
+ s.e.GET(fmt.Sprintf("/api/admin/form/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/form/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/test/form_test.go-e b/test/form_test.go-e
new file mode 100644
index 000000000..b2f698d85
--- /dev/null
+++ b/test/form_test.go-e
@@ -0,0 +1,115 @@
+package test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+)
+
+type form struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestForm(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ f1 := createSimpleForm(s, u, "form1-"+s.id, "test-"+s.id)
+ f2 := createForm(s, u, "form2-"+s.id)
+
+ deleteForm(s, f1.ID, false)
+ deleteForm(s, f2.ID, true)
+}
+
+func createForm(s *session, u *user, name string) *form {
+ now := time.Now()
+ f := &form{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/form/update").WithJSON(f).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/form/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(f.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == f.Name {
+ f.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return f
+}
+
+func createSimpleForm(s *session, u *user, name, fieldName string) *form {
+ f := createForm(s, u, name)
+ f.Data = simpleFormData(fieldName)
+ return updateForm(s, f)
+}
+
+func simpleFormData(fieldName string) map[string]interface{} {
+ j := fmt.Sprintf(`{
+ "formSrc": {
+ "components": {
+ "5zvr98w21yynozx60nhmc": {
+ "_compId": "HC2",
+ "_order": 0,
+ "autocomplete": "on",
+ "help": "test-help",
+ "label": "test-label",
+ "name": "%s",
+ "placeholder": "test-placeholder",
+ "validate": {
+ "required": true
+ }
+ }
+ },
+ "v": 2
+ }
+ }`, fieldName)
+
+ var result map[string]interface{}
+
+ err := json.Unmarshal([]byte(j), &result)
+ if err != nil {
+ return nil
+ }
+
+ return result
+}
+
+func updateForm(s *session, f *form) *form {
+ s.e.POST("/api/admin/form/update").WithQuery("id", f.ID).WithJSON(f).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(f))
+ s.e.GET("/api/admin/form/{id}").WithPath("id", f.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return f
+}
+
+func deleteForm(s *session, id string, expectEmptyList bool) {
+
+ s.e.GET(fmt.Sprintf("/api/admin/form/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/form/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/test/permissions_test.go b/test/permissions_test.go
new file mode 100644
index 000000000..9dfaa3fc2
--- /dev/null
+++ b/test/permissions_test.go
@@ -0,0 +1,32 @@
+package test
+
+type role int
+
+type permission []byte
+
+type groupAndOthers struct {
+ //Allowed to modify: @Owner only!
+ Group role `json:"group,omitempty"`
+ //Rights pattern: group others
+ // -- --
+ //default value is: ----
+ //example for group and others with read perm: r-r-
+ //Allowed to modify: @Owner only!
+ Rights permission `json:"rights,omitempty"`
+}
+
+type permissions struct {
+ //Allowed to modify: @Owner only!
+ Owner string `json:"owner,omitempty"`
+ //Grant can be modified by the owner only and is an optional field to whitelist user', s directly
+ //Allowed to modify: everyone with write rights!
+ Grant map[string]permission `json:"grant,omitempty"`
+
+ GroupAndOthers groupAndOthers `json:"groupAndOthers,omitempty"`
+ //Accessible by everyone who has the ID
+ //Allowed to modify: everyone with write rights!
+ PublicByID permission `json:"publicByID,omitempty"`
+
+ //Execute only! If read or write not set.
+ Published bool `json:"published"`
+}
diff --git a/test/permissions_test.go-e b/test/permissions_test.go-e
new file mode 100644
index 000000000..9dfaa3fc2
--- /dev/null
+++ b/test/permissions_test.go-e
@@ -0,0 +1,32 @@
+package test
+
+type role int
+
+type permission []byte
+
+type groupAndOthers struct {
+ //Allowed to modify: @Owner only!
+ Group role `json:"group,omitempty"`
+ //Rights pattern: group others
+ // -- --
+ //default value is: ----
+ //example for group and others with read perm: r-r-
+ //Allowed to modify: @Owner only!
+ Rights permission `json:"rights,omitempty"`
+}
+
+type permissions struct {
+ //Allowed to modify: @Owner only!
+ Owner string `json:"owner,omitempty"`
+ //Grant can be modified by the owner only and is an optional field to whitelist user', s directly
+ //Allowed to modify: everyone with write rights!
+ Grant map[string]permission `json:"grant,omitempty"`
+
+ GroupAndOthers groupAndOthers `json:"groupAndOthers,omitempty"`
+ //Accessible by everyone who has the ID
+ //Allowed to modify: everyone with write rights!
+ PublicByID permission `json:"publicByID,omitempty"`
+
+ //Execute only! If read or write not set.
+ Published bool `json:"published"`
+}
diff --git a/test/template_test.go b/test/template_test.go
new file mode 100644
index 000000000..30070b734
--- /dev/null
+++ b/test/template_test.go
@@ -0,0 +1,106 @@
+package test
+
+import (
+ "fmt"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+)
+
+type template struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestTemplate(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ t1 := createSimpleTemplate(s, u, "template1-"+s.id, "test/assets/test_template.odt")
+ t2 := createTemplate(s, u, "template2-"+s.id)
+
+ deleteTemplate(s, t1.ID, false)
+ deleteTemplate(s, t2.ID, true)
+}
+
+func createSimpleTemplate(s *session, u *user, name, path string) *template {
+
+ fileContent, err := Asset(path)
+ if err != nil {
+ s.t.Errorf("Cannot upload asset %s", err)
+ }
+
+ t := createTemplate(s, u, name)
+ uploadTemplateFile(s, t, "en", fileContent, "application/vnd.oasis.opendocument.text", filepath.Base(name))
+
+ return t
+}
+
+func createTemplate(s *session, u *user, name string) *template {
+ now := time.Now()
+
+ t := &template{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/template/update").WithJSON(t).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/template/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(t.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == t.Name {
+ t.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return t
+}
+
+func updateTemplate(s *session, t *template) *template {
+ s.e.POST("/api/admin/template/update").WithQuery("id", t.ID).WithJSON(t).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(t))
+ s.e.GET("/api/admin/template/{id}").WithPath("id", t.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return t
+}
+
+func uploadTemplateFile(s *session, t *template, lang string, b []byte, contentType string, fileName string) {
+ s.e.POST("/api/admin/template/upload/{id}/{lang}").WithPath("id", t.ID).WithPath("lang", lang).WithBytes(b).
+ WithHeader("Content-Type", contentType).
+ WithHeader("File-Name", fileName).
+ WithHeader("Content-Length", strconv.Itoa(len(b))).
+ Expect().Status(http.StatusOK)
+
+ r := s.e.GET("/api/admin/template/{id}").WithPath("id", t.ID).
+ Expect().Status(http.StatusOK).JSON()
+ r.Path("$.data.en.name").Equal(fileName)
+ r.Path("$.data.en.contentType").Equal(contentType)
+
+}
+
+func deleteTemplate(s *session, id string, expectEmptyList bool) {
+ s.e.GET(fmt.Sprintf("/api/admin/template/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/template/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/test/template_test.go-e b/test/template_test.go-e
new file mode 100644
index 000000000..30070b734
--- /dev/null
+++ b/test/template_test.go-e
@@ -0,0 +1,106 @@
+package test
+
+import (
+ "fmt"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+)
+
+type template struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestTemplate(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ t1 := createSimpleTemplate(s, u, "template1-"+s.id, "test/assets/test_template.odt")
+ t2 := createTemplate(s, u, "template2-"+s.id)
+
+ deleteTemplate(s, t1.ID, false)
+ deleteTemplate(s, t2.ID, true)
+}
+
+func createSimpleTemplate(s *session, u *user, name, path string) *template {
+
+ fileContent, err := Asset(path)
+ if err != nil {
+ s.t.Errorf("Cannot upload asset %s", err)
+ }
+
+ t := createTemplate(s, u, name)
+ uploadTemplateFile(s, t, "en", fileContent, "application/vnd.oasis.opendocument.text", filepath.Base(name))
+
+ return t
+}
+
+func createTemplate(s *session, u *user, name string) *template {
+ now := time.Now()
+
+ t := &template{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/template/update").WithJSON(t).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/template/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(t.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == t.Name {
+ t.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return t
+}
+
+func updateTemplate(s *session, t *template) *template {
+ s.e.POST("/api/admin/template/update").WithQuery("id", t.ID).WithJSON(t).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(t))
+ s.e.GET("/api/admin/template/{id}").WithPath("id", t.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return t
+}
+
+func uploadTemplateFile(s *session, t *template, lang string, b []byte, contentType string, fileName string) {
+ s.e.POST("/api/admin/template/upload/{id}/{lang}").WithPath("id", t.ID).WithPath("lang", lang).WithBytes(b).
+ WithHeader("Content-Type", contentType).
+ WithHeader("File-Name", fileName).
+ WithHeader("Content-Length", strconv.Itoa(len(b))).
+ Expect().Status(http.StatusOK)
+
+ r := s.e.GET("/api/admin/template/{id}").WithPath("id", t.ID).
+ Expect().Status(http.StatusOK).JSON()
+ r.Path("$.data.en.name").Equal(fileName)
+ r.Path("$.data.en.contentType").Equal(contentType)
+
+}
+
+func deleteTemplate(s *session, id string, expectEmptyList bool) {
+ s.e.GET(fmt.Sprintf("/api/admin/template/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/template/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/test/unattended_workflow_test.go b/test/unattended_workflow_test.go
new file mode 100644
index 000000000..cd8739ac3
--- /dev/null
+++ b/test/unattended_workflow_test.go
@@ -0,0 +1,82 @@
+package test
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ w "git.proxeus.com/core/central/sys/workflow"
+)
+
+func TestUnattendedWorkflow(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+
+ login(s, u)
+ apiKey, summary := createApiKey(s, u, "test-"+s.id)
+ w := createWorkflow(s, u, "workflow-"+s.id)
+ f := createSimpleForm(s, u, "form-"+s.id, "test_name")
+ tpl := createSimpleTemplate(s, u, "template-"+s.id, "test/assets/test_template.odt")
+ w.Data = simpleWorkflowData(s.id, f.ID, tpl.ID)
+ updateWorkflow(s, w)
+ logout(s)
+
+ token := getSessionToken(s, u.username, apiKey)
+ id := listFirstDocument(s, token)
+ schema := getDocumentSchema(s, token, id)
+
+ data := map[string]interface{}{}
+ i := 0
+ for k, _ := range schema {
+ data[k] = fmt.Sprintf("value-%d", i)
+ i++
+ }
+
+ r := executeAllAtOnce(s, token, id, data)
+
+ expected, err := Asset("test/assets/test_expected.pdf")
+ if err != nil {
+ s.t.Errorf("Cannot upload asset %s", err)
+ }
+
+ if bytes.Compare(cleanPDF(r), cleanPDF(expected)) != 0 {
+ t.Errorf("Wrong pdf result")
+ }
+
+ login(s, u)
+ deleteWorkflow(s, w.ID, true)
+ deleteApiKey(s, u, summary)
+ deleteUser(s, u)
+}
+
+type workflowItem struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Price uint64 `json:"price" storm:"index"`
+
+ Data *w.Workflow `json:"data"`
+ OwnerEthAddress string `json:"ownerEthAddress"` //only used in frontend
+ Deactivated bool `json:"deactivated"`
+}
+
+func listFirstDocument(s *session, token string) string {
+ return s.e.GET("/api/document/list").WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).JSON().Array().First().Object().Value("id").String().Raw()
+}
+
+func getDocumentSchema(s *session, token, id string) map[string]interface{} {
+ schema := s.e.GET("/api/document/{id}/allAtOnce/schema").WithPath("id", id).WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).JSON().Object().Path("$.workflow.data").Object()
+ schema.ContainsKey("test_name")
+ return schema.Raw()
+}
+
+func executeAllAtOnce(s *session, token, id string, data map[string]interface{}) []byte {
+ r := s.e.POST("/api/document/{id}/allAtOnce").WithPath("id", id).WithHeader("Authorization", "Bearer "+token).WithJSON(data).Expect().ContentType("application/pdf").Body().Raw()
+
+ return []byte(r)
+}
diff --git a/test/unattended_workflow_test.go-e b/test/unattended_workflow_test.go-e
new file mode 100644
index 000000000..cd8739ac3
--- /dev/null
+++ b/test/unattended_workflow_test.go-e
@@ -0,0 +1,82 @@
+package test
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ w "git.proxeus.com/core/central/sys/workflow"
+)
+
+func TestUnattendedWorkflow(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+
+ login(s, u)
+ apiKey, summary := createApiKey(s, u, "test-"+s.id)
+ w := createWorkflow(s, u, "workflow-"+s.id)
+ f := createSimpleForm(s, u, "form-"+s.id, "test_name")
+ tpl := createSimpleTemplate(s, u, "template-"+s.id, "test/assets/test_template.odt")
+ w.Data = simpleWorkflowData(s.id, f.ID, tpl.ID)
+ updateWorkflow(s, w)
+ logout(s)
+
+ token := getSessionToken(s, u.username, apiKey)
+ id := listFirstDocument(s, token)
+ schema := getDocumentSchema(s, token, id)
+
+ data := map[string]interface{}{}
+ i := 0
+ for k, _ := range schema {
+ data[k] = fmt.Sprintf("value-%d", i)
+ i++
+ }
+
+ r := executeAllAtOnce(s, token, id, data)
+
+ expected, err := Asset("test/assets/test_expected.pdf")
+ if err != nil {
+ s.t.Errorf("Cannot upload asset %s", err)
+ }
+
+ if bytes.Compare(cleanPDF(r), cleanPDF(expected)) != 0 {
+ t.Errorf("Wrong pdf result")
+ }
+
+ login(s, u)
+ deleteWorkflow(s, w.ID, true)
+ deleteApiKey(s, u, summary)
+ deleteUser(s, u)
+}
+
+type workflowItem struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Price uint64 `json:"price" storm:"index"`
+
+ Data *w.Workflow `json:"data"`
+ OwnerEthAddress string `json:"ownerEthAddress"` //only used in frontend
+ Deactivated bool `json:"deactivated"`
+}
+
+func listFirstDocument(s *session, token string) string {
+ return s.e.GET("/api/document/list").WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).JSON().Array().First().Object().Value("id").String().Raw()
+}
+
+func getDocumentSchema(s *session, token, id string) map[string]interface{} {
+ schema := s.e.GET("/api/document/{id}/allAtOnce/schema").WithPath("id", id).WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).JSON().Object().Path("$.workflow.data").Object()
+ schema.ContainsKey("test_name")
+ return schema.Raw()
+}
+
+func executeAllAtOnce(s *session, token, id string, data map[string]interface{}) []byte {
+ r := s.e.POST("/api/document/{id}/allAtOnce").WithPath("id", id).WithHeader("Authorization", "Bearer "+token).WithJSON(data).Expect().ContentType("application/pdf").Body().Raw()
+
+ return []byte(r)
+}
diff --git a/test/user_test.go b/test/user_test.go
new file mode 100644
index 000000000..a8acec538
--- /dev/null
+++ b/test/user_test.go
@@ -0,0 +1,114 @@
+package test
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "testing"
+
+ "git.proxeus.com/core/central/main/handlers/api"
+ uuid "github.com/satori/go.uuid"
+ "gopkg.in/gavv/httpexpect.v2"
+)
+
+var serverURL string
+
+type user struct {
+ uuid string
+ username string
+ password string
+}
+
+type session struct {
+ id string
+ t *testing.T
+ e *httpexpect.Expect
+}
+
+func init() {
+ serverURL = os.Getenv("PROXEUS_URL")
+}
+
+func new(t *testing.T, serverURL string) *session {
+ return &session{
+ id: uuid.NewV4().String(),
+ t: t,
+ e: httpexpect.New(t, serverURL),
+ }
+}
+
+func TestUser(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ logout(s)
+ login(s, u)
+ deleteUser(s, u)
+}
+
+func registerTestUser(s *session) *user {
+ // Register test user
+ u := &user{
+ username: fmt.Sprintf("test%s@example.com", s.id),
+ password: s.id,
+ }
+
+ s.t.Logf("Starting test %s", s.id)
+ s.t.Logf("User %s %s", u.username, u.password)
+
+ tr := &api.TokenRequest{
+ Email: u.username,
+ }
+
+ r := s.e.POST("/api/register").WithJSON(tr).Expect()
+
+ r.Status(http.StatusOK)
+ r.Header("X-Test-Token").NotEmpty() // This is only true in TESTMODE
+ registrationToken := r.Header("X-Test-Token").Raw()
+
+ p := &struct {
+ Password string `json:"password"`
+ }{
+ Password: u.password,
+ }
+
+ r = s.e.POST("/api/register/" + registrationToken).WithJSON(p).
+ Expect().
+ Status(http.StatusOK)
+
+ return u
+}
+
+func login(s *session, u *user) {
+ l := &struct {
+ Email string `json:"email" form:"email"`
+ Password string `json:"password" form:"password"`
+ }{
+ Email: u.username,
+ Password: u.password,
+ }
+ s.e.POST("/api/login").WithJSON(l).Expect().Status(http.StatusOK)
+
+ me := s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Object()
+ me.ValueEqual("email", u.username)
+
+ u.uuid = me.Value("id").String().Raw()
+}
+
+func logout(s *session) {
+ s.e.POST("/api/logout").Expect().Status(http.StatusOK)
+ s.e.GET("/api/me").Expect().Status(http.StatusNotFound)
+}
+
+func deleteUser(s *session, u *user) {
+ s.e.POST("/api/user/delete").Expect().Status(http.StatusOK)
+
+ l := &struct {
+ Email string `json:"email" form:"email"`
+ Password string `json:"password" form:"password"`
+ }{
+ Email: u.username,
+ Password: u.password,
+ }
+ s.e.POST("/api/login").WithJSON(l).Expect().Status(http.StatusBadRequest)
+}
diff --git a/test/user_test.go-e b/test/user_test.go-e
new file mode 100644
index 000000000..a8acec538
--- /dev/null
+++ b/test/user_test.go-e
@@ -0,0 +1,114 @@
+package test
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "testing"
+
+ "git.proxeus.com/core/central/main/handlers/api"
+ uuid "github.com/satori/go.uuid"
+ "gopkg.in/gavv/httpexpect.v2"
+)
+
+var serverURL string
+
+type user struct {
+ uuid string
+ username string
+ password string
+}
+
+type session struct {
+ id string
+ t *testing.T
+ e *httpexpect.Expect
+}
+
+func init() {
+ serverURL = os.Getenv("PROXEUS_URL")
+}
+
+func new(t *testing.T, serverURL string) *session {
+ return &session{
+ id: uuid.NewV4().String(),
+ t: t,
+ e: httpexpect.New(t, serverURL),
+ }
+}
+
+func TestUser(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ logout(s)
+ login(s, u)
+ deleteUser(s, u)
+}
+
+func registerTestUser(s *session) *user {
+ // Register test user
+ u := &user{
+ username: fmt.Sprintf("test%s@example.com", s.id),
+ password: s.id,
+ }
+
+ s.t.Logf("Starting test %s", s.id)
+ s.t.Logf("User %s %s", u.username, u.password)
+
+ tr := &api.TokenRequest{
+ Email: u.username,
+ }
+
+ r := s.e.POST("/api/register").WithJSON(tr).Expect()
+
+ r.Status(http.StatusOK)
+ r.Header("X-Test-Token").NotEmpty() // This is only true in TESTMODE
+ registrationToken := r.Header("X-Test-Token").Raw()
+
+ p := &struct {
+ Password string `json:"password"`
+ }{
+ Password: u.password,
+ }
+
+ r = s.e.POST("/api/register/" + registrationToken).WithJSON(p).
+ Expect().
+ Status(http.StatusOK)
+
+ return u
+}
+
+func login(s *session, u *user) {
+ l := &struct {
+ Email string `json:"email" form:"email"`
+ Password string `json:"password" form:"password"`
+ }{
+ Email: u.username,
+ Password: u.password,
+ }
+ s.e.POST("/api/login").WithJSON(l).Expect().Status(http.StatusOK)
+
+ me := s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Object()
+ me.ValueEqual("email", u.username)
+
+ u.uuid = me.Value("id").String().Raw()
+}
+
+func logout(s *session) {
+ s.e.POST("/api/logout").Expect().Status(http.StatusOK)
+ s.e.GET("/api/me").Expect().Status(http.StatusNotFound)
+}
+
+func deleteUser(s *session, u *user) {
+ s.e.POST("/api/user/delete").Expect().Status(http.StatusOK)
+
+ l := &struct {
+ Email string `json:"email" form:"email"`
+ Password string `json:"password" form:"password"`
+ }{
+ Email: u.username,
+ Password: u.password,
+ }
+ s.e.POST("/api/login").WithJSON(l).Expect().Status(http.StatusBadRequest)
+}
diff --git a/test/workflow_test.go b/test/workflow_test.go
new file mode 100644
index 000000000..18f802366
--- /dev/null
+++ b/test/workflow_test.go
@@ -0,0 +1,131 @@
+package test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+)
+
+type workflow struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestWorkflow(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ w1 := createWorkflow(s, u, "workflow1-"+s.id)
+ w2 := createWorkflow(s, u, "workflow2-"+s.id)
+
+ f := createForm(s, u, "form-"+s.id)
+ tpl := createTemplate(s, u, "template-"+s.id)
+ w1.Data = simpleWorkflowData(s.id, f.ID, tpl.ID)
+ updateWorkflow(s, w1)
+
+ deleteWorkflow(s, w1.ID, false)
+ deleteWorkflow(s, w2.ID, true)
+}
+
+func createWorkflow(s *session, u *user, name string) *workflow {
+ now := time.Now()
+ f := &workflow{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/workflow/update").WithJSON(f).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/workflow/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(f.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == f.Name {
+ f.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return f
+}
+
+func simpleWorkflowData(id string, formId, templateId string) map[string]interface{} {
+ j := fmt.Sprintf(`{
+ "flow": {
+ "start": {
+ "node": "%s",
+ "p": {
+ "x": -438,
+ "y": -100
+ }
+ },
+ "nodes": {
+ "%s": {
+ "id": "%s",
+ "name": "test",
+ "type": "form",
+ "conns": [
+ {
+ "id": "%s"
+ }
+ ],
+ "p": {
+ "x": -225,
+ "y": -102
+ }
+ },
+ "%s": {
+ "id": "%s",
+ "name": "test",
+ "type": "template",
+ "p": {
+ "x": -18,
+ "y": -131
+ }
+ }
+ }
+ }
+ }`, formId, formId, formId, templateId, templateId, templateId)
+
+ var result map[string]interface{}
+
+ err := json.Unmarshal([]byte(j), &result)
+ if err != nil {
+ return nil
+ }
+
+ return result
+}
+
+func updateWorkflow(s *session, f *workflow) *workflow {
+ s.e.POST("/api/admin/workflow/update").WithQuery("id", f.ID).WithJSON(f).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(f))
+ s.e.GET("/api/admin/workflow/{id}").WithPath("id", f.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return f
+}
+
+func deleteWorkflow(s *session, id string, expectEmptyList bool) {
+
+ s.e.GET(fmt.Sprintf("/api/admin/workflow/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/workflow/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/test/workflow_test.go-e b/test/workflow_test.go-e
new file mode 100644
index 000000000..18f802366
--- /dev/null
+++ b/test/workflow_test.go-e
@@ -0,0 +1,131 @@
+package test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+)
+
+type workflow struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestWorkflow(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ w1 := createWorkflow(s, u, "workflow1-"+s.id)
+ w2 := createWorkflow(s, u, "workflow2-"+s.id)
+
+ f := createForm(s, u, "form-"+s.id)
+ tpl := createTemplate(s, u, "template-"+s.id)
+ w1.Data = simpleWorkflowData(s.id, f.ID, tpl.ID)
+ updateWorkflow(s, w1)
+
+ deleteWorkflow(s, w1.ID, false)
+ deleteWorkflow(s, w2.ID, true)
+}
+
+func createWorkflow(s *session, u *user, name string) *workflow {
+ now := time.Now()
+ f := &workflow{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/workflow/update").WithJSON(f).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/workflow/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(f.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == f.Name {
+ f.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return f
+}
+
+func simpleWorkflowData(id string, formId, templateId string) map[string]interface{} {
+ j := fmt.Sprintf(`{
+ "flow": {
+ "start": {
+ "node": "%s",
+ "p": {
+ "x": -438,
+ "y": -100
+ }
+ },
+ "nodes": {
+ "%s": {
+ "id": "%s",
+ "name": "test",
+ "type": "form",
+ "conns": [
+ {
+ "id": "%s"
+ }
+ ],
+ "p": {
+ "x": -225,
+ "y": -102
+ }
+ },
+ "%s": {
+ "id": "%s",
+ "name": "test",
+ "type": "template",
+ "p": {
+ "x": -18,
+ "y": -131
+ }
+ }
+ }
+ }
+ }`, formId, formId, formId, templateId, templateId, templateId)
+
+ var result map[string]interface{}
+
+ err := json.Unmarshal([]byte(j), &result)
+ if err != nil {
+ return nil
+ }
+
+ return result
+}
+
+func updateWorkflow(s *session, f *workflow) *workflow {
+ s.e.POST("/api/admin/workflow/update").WithQuery("id", f.ID).WithJSON(f).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(f))
+ s.e.GET("/api/admin/workflow/{id}").WithPath("id", f.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return f
+}
+
+func deleteWorkflow(s *session, id string, expectEmptyList bool) {
+
+ s.e.GET(fmt.Sprintf("/api/admin/workflow/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/workflow/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/ui/core/src/assets/fonts/.!86115!fontawesome-webfont.woff2 b/ui/core/public/static/.!87089!favicon.ico
similarity index 100%
rename from ui/core/src/assets/fonts/.!86115!fontawesome-webfont.woff2
rename to ui/core/public/static/.!87089!favicon.ico
diff --git a/ui/core/src/assets/fonts/.!86116!WorkSans-SemiBold.ttf b/ui/core/public/static/.!87090!proxeus_white.jpg
similarity index 100%
rename from ui/core/src/assets/fonts/.!86116!WorkSans-SemiBold.ttf
rename to ui/core/public/static/.!87090!proxeus_white.jpg
diff --git a/ui/core/src/assets/fonts/.!86117!WorkSans-Regular.woff2 b/ui/core/public/static/.!87091!proxeus_blue.jpg
similarity index 100%
rename from ui/core/src/assets/fonts/.!86117!WorkSans-Regular.woff2
rename to ui/core/public/static/.!87091!proxeus_blue.jpg
diff --git a/ui/core/public/static/proxeus_blue.jpg b/ui/core/public/static/proxeus_blue.jpg
new file mode 100644
index 000000000..8b4ae9a66
Binary files /dev/null and b/ui/core/public/static/proxeus_blue.jpg differ
diff --git a/ui/core/public/static/proxeus_white.jpg b/ui/core/public/static/proxeus_white.jpg
new file mode 100644
index 000000000..ed5d123c3
Binary files /dev/null and b/ui/core/public/static/proxeus_white.jpg differ
diff --git a/ui/core/src/FrontendApp.vue b/ui/core/src/FrontendApp.vue
index c75206197..171d96ac2 100644
--- a/ui/core/src/FrontendApp.vue
+++ b/ui/core/src/FrontendApp.vue
@@ -11,7 +11,7 @@
'
},
nodeIconMap: {
- ibmsender: 'fcn-ibmsender node-icon mdi mdi-send',
- mailsender: 'fcn-ibmsender node-icon mdi mdi-send',
- priceretriever: 'fcn-ibmsender node-icon mdi mdi-send',
+ mailsender: 'fcn-externalnode node-icon mdi mdi-send',
+ priceretriever: 'fcn-externalnode node-icon mdi mdi-send',
condition: 'fcn-condition node-icon mdi mdi-circle-outline',
user: 'fcn-usr node-icon mdi mdi-account',
form: 'fcn-form node-icon mdi mdi-view-quilt',
@@ -1372,59 +1369,6 @@ function condition(){
'dblclick': _.onDblClick
}
},
- ibmsender: {
- connections: {
- from: [
- {
- node: {
- color: {
- background: '#8688ff',
- highlight: { background: '#5f5ff0' },
- hover: { background: '#5f5ff0' }
- },
- borderWidthSelected: 3
- },
- edge: {
- color: {
- color: '#8688ff',
- highlight: '#a8a5ff',
- hover: '#a8a5ff'
- }
- }
- }],
- to: Infinity,
- space: 1.1
- },
- font: {
- color: '#343434',
- size: 15,
- mod: 'bold',
- bold: {
- color: '#343434',
- size: 14, // px
- face: 'arial',
- vadjust: 0,
- mod: 'bold'
- }
- },
- icon: {
- face: 'Material Design Icons',
- code: '\uf48a',
- color: '#5353c0'
- },
- events: {
- 'hoverIn': function () {
- },
- 'hoverOut': function () {
- },
- 'remove': function () {
- },
- 'click': function () {
- },
- 'dblclick': function () {
- }
- }
- },
mailsender: {
connections: {
from: [
diff --git a/ui/core/src/views/Workflow.vue-e b/ui/core/src/views/Workflow.vue-e
index b9cc50ddb..d2ed24f74 100644
--- a/ui/core/src/views/Workflow.vue-e
+++ b/ui/core/src/views/Workflow.vue-e
@@ -427,8 +427,6 @@ export default {
}
}
}
- console.log('elements')
- console.log(elements)
if (elements.length) {
this.showPublishResponseDialog(null, elements)
}
@@ -1188,9 +1186,8 @@ function condition(){
'
'
},
nodeIconMap: {
- ibmsender: 'fcn-ibmsender node-icon mdi mdi-send',
- mailsender: 'fcn-ibmsender node-icon mdi mdi-send',
- priceretriever: 'fcn-ibmsender node-icon mdi mdi-send',
+ mailsender: 'fcn-externalnode node-icon mdi mdi-send',
+ priceretriever: 'fcn-externalnode node-icon mdi mdi-send',
condition: 'fcn-condition node-icon mdi mdi-circle-outline',
user: 'fcn-usr node-icon mdi mdi-account',
form: 'fcn-form node-icon mdi mdi-view-quilt',
@@ -1372,59 +1369,6 @@ function condition(){
'dblclick': _.onDblClick
}
},
- ibmsender: {
- connections: {
- from: [
- {
- node: {
- color: {
- background: '#8688ff',
- highlight: { background: '#5f5ff0' },
- hover: { background: '#5f5ff0' }
- },
- borderWidthSelected: 3
- },
- edge: {
- color: {
- color: '#8688ff',
- highlight: '#a8a5ff',
- hover: '#a8a5ff'
- }
- }
- }],
- to: Infinity,
- space: 1.1
- },
- font: {
- color: '#343434',
- size: 15,
- mod: 'bold',
- bold: {
- color: '#343434',
- size: 14, // px
- face: 'arial',
- vadjust: 0,
- mod: 'bold'
- }
- },
- icon: {
- face: 'Material Design Icons',
- code: '\uf48a',
- color: '#5353c0'
- },
- events: {
- 'hoverIn': function () {
- },
- 'hoverOut': function () {
- },
- 'remove': function () {
- },
- 'click': function () {
- },
- 'dblclick': function () {
- }
- }
- },
mailsender: {
connections: {
from: [
diff --git a/ui/core/src/views/appDependentComponents/SettingsInner.vue b/ui/core/src/views/appDependentComponents/SettingsInner.vue
index 69cd02b08..973a7cf3f 100644
--- a/ui/core/src/views/appDependentComponents/SettingsInner.vue
+++ b/ui/core/src/views/appDependentComponents/SettingsInner.vue
@@ -30,6 +30,10 @@
{{$t('Document Service URL explanation','Set the Document Service URL which will be used to render documents.')}}
{{$t('Platform Domain explanation','Set the Domain this Platform instance is identifying as (used for example for sending links to this instance)')}}
+
+
+
{{$t('Default workflow ids explanation','Comma separated ids of workflows you want your new users to inherit (if any)')}}
+
{{$t('Blockchain settings')}}
@@ -38,6 +42,12 @@
{{$t('Infura API Key explanation','API Key to access Infura node.')}}
{{$t('Blockchain contract address explanation','Set the ethereum contract address which will be used to register files and verify them.')}}
+
+
{{$t('Airdrop Enable Explanation','Enables/Disables the XES & Ether airdrop feature for new users on ropsten. The Amount and Wallet to be used is configured in the platform configuration.')}}
+
+
{{$t('Airdrop Amount XES Explanation','Set the amount of XES to be airdropped to newly registered users.')}}
+
+
{{$t('Airdrop Amount Ether Explanation','Set the amount of Ether to be airdropped to newly registered users.')}}
{{$t('Email settings')}}
@@ -226,7 +236,17 @@ export default {
configured: false,
initialized: false,
importResultsAvailable: false,
- results: null
+ results: null,
+ airdropoptions: [
+ {
+ label: 'Airdrop enabled on Ropsten',
+ value: 'true'
+ },
+ {
+ label: 'Airdrop disabled',
+ value: 'false'
+ }
+ ]
}
}
}
diff --git a/ui/core/src/views/appDependentComponents/SettingsInner.vue-e b/ui/core/src/views/appDependentComponents/SettingsInner.vue-e
index 69cd02b08..973a7cf3f 100644
--- a/ui/core/src/views/appDependentComponents/SettingsInner.vue-e
+++ b/ui/core/src/views/appDependentComponents/SettingsInner.vue-e
@@ -30,6 +30,10 @@
{{$t('Document Service URL explanation','Set the Document Service URL which will be used to render documents.')}}
{{$t('Platform Domain explanation','Set the Domain this Platform instance is identifying as (used for example for sending links to this instance)')}}
+
+
+
{{$t('Default workflow ids explanation','Comma separated ids of workflows you want your new users to inherit (if any)')}}
+
{{$t('Blockchain settings')}}
@@ -38,6 +42,12 @@
{{$t('Infura API Key explanation','API Key to access Infura node.')}}
{{$t('Blockchain contract address explanation','Set the ethereum contract address which will be used to register files and verify them.')}}
+
+
{{$t('Airdrop Enable Explanation','Enables/Disables the XES & Ether airdrop feature for new users on ropsten. The Amount and Wallet to be used is configured in the platform configuration.')}}
+
+
{{$t('Airdrop Amount XES Explanation','Set the amount of XES to be airdropped to newly registered users.')}}
+
+
{{$t('Airdrop Amount Ether Explanation','Set the amount of Ether to be airdropped to newly registered users.')}}
{{$t('Email settings')}}
@@ -226,7 +236,17 @@ export default {
configured: false,
initialized: false,
importResultsAvailable: false,
- results: null
+ results: null,
+ airdropoptions: [
+ {
+ label: 'Airdrop enabled on Ropsten',
+ value: 'true'
+ },
+ {
+ label: 'Airdrop disabled',
+ value: 'false'
+ }
+ ]
}
}
}
diff --git a/ui/core/src/views/appDependentComponents/Sidebar.vue b/ui/core/src/views/appDependentComponents/Sidebar.vue
index 911eec431..f4fc6f284 100644
--- a/ui/core/src/views/appDependentComponents/Sidebar.vue
+++ b/ui/core/src/views/appDependentComponents/Sidebar.vue
@@ -3,20 +3,11 @@
-
- logo
-
-
-
-
-
+
+