11 Commits

13 changed files with 299 additions and 78 deletions

View File

@ -16,17 +16,16 @@ docker-image:
docker-run: docker-run:
docker run \ docker run \
--rm -it \ --rm -it \
-p 8080:8080 \ -p 3000:3000 \
-p 2525:2525 \
--tmpfs /app/data \ --tmpfs /app/data \
fake-sms:latest fake-sms:latest
docker-release: docker-release:
docker tag fake-sms:latest bornholm/fake-sms:latest docker tag fake-sms:latest cadoles/fake-sms:latest
docker tag fake-sms:latest bornholm/fake-sms:$(DOCKER_DATE_TAG) docker tag fake-sms:latest cadoles/fake-sms:$(DOCKER_DATE_TAG)
docker login docker login
docker push bornholm/fake-sms:latest docker push cadoles/fake-sms:latest
docker push bornholm/fake-sms:$(DOCKER_DATE_TAG) docker push cadoles/fake-sms:$(DOCKER_DATE_TAG)
test: test:
go test -v -race ./... go test -v -race ./...

View File

@ -7,7 +7,7 @@ Serveur d'envoi de SMS factice pour le développement avec interface web.
### Avec Docker ### Avec Docker
```bash ```bash
docker run -it --rm -p 3000:3000 bornholm/fake-sms docker run -it --rm -p 3000:3000 cadoles/fake-sms
``` ```
L'interface Web sera accessible à l'adresse http://localhost:3000/. L'interface Web sera accessible à l'adresse http://localhost:3000/.
@ -40,23 +40,8 @@ powow:
# Clé d'API à utiliser par les clients Powow utilisant le mock # Clé d'API à utiliser par les clients Powow utilisant le mock
apiKey: powow apiKey: powow
# Modèles de SMS transactionnels # La création/mise à jour de modèles de SMS s'effectue via les méthodes TransactionalSMS.Create et TransactionalSMS.Update.
# Voir https://powow4.iroquois.fr/user/docs/api/#create-transactional-sms # Voir le fichier ./misc/powow.http pour un exemple de requête.
# et https://powow4.iroquois.fr/user/docs/api/#update-transactional-sms
#
# L'identifiant (SmsID) de chaque modèle est son index dans le tableau.
sms:
- name: Powow SMS
from: FakeSMS
# Modèle de contenu pour le SMS avec patrons d'insertion
# Voir https://powow4.iroquois.fr/user/docs/api/#send-transactional-sms, "About the CustomData parameter"
content: |
Bonjour %Subscriber:Firstname%,
Lorem ipsum dolor sit amet...
# Cet attribut n'est pas utilisé dans le cadre du mock
shortLink: false
``` ```
### Variables d'environnement ### Variables d'environnement

View File

@ -8,6 +8,8 @@ import (
"forge.cadoles.com/Cadoles/fake-sms/internal/command" "forge.cadoles.com/Cadoles/fake-sms/internal/command"
"forge.cadoles.com/Cadoles/fake-sms/internal/config" "forge.cadoles.com/Cadoles/fake-sms/internal/config"
"forge.cadoles.com/Cadoles/fake-sms/internal/model"
"forge.cadoles.com/Cadoles/fake-sms/internal/model/powow"
"forge.cadoles.com/Cadoles/fake-sms/internal/query" "forge.cadoles.com/Cadoles/fake-sms/internal/query"
"forge.cadoles.com/Cadoles/fake-sms/internal/storm" "forge.cadoles.com/Cadoles/fake-sms/internal/storm"
"gitlab.com/wpetit/goweb/cqrs" "gitlab.com/wpetit/goweb/cqrs"
@ -40,6 +42,10 @@ func getServiceContainer(conf *config.Config) (*service.Container, error) {
ctn.Provide(storm.ServiceName, storm.ServiceProvider( ctn.Provide(storm.ServiceName, storm.ServiceProvider(
storm.WithPath(conf.Data.Path), storm.WithPath(conf.Data.Path),
storm.WithObjects(
&model.SMS{},
&powow.SMSTemplate{},
),
)) ))
ctn.Provide(cqrs.ServiceName, cqrs.ServiceProvider()) ctn.Provide(cqrs.ServiceName, cqrs.ServiceProvider())

View File

@ -6,10 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" . -}}{{- end}}</title> <title>{{block "title" . -}}{{- end}}</title>
{{- block "head_style" . -}} {{- block "head_style" . -}}
<link rel="stylesheet" href="/css/main.css" /> <link rel="stylesheet" href="{{ .BaseURL }}/css/main.css" />
{{end}} {{end}}
{{- block "head_script" . -}} {{- block "head_script" . -}}
<script type="text/javascript" src="/main.js"></script> <script type="text/javascript" src="{{ .BaseURL }}/main.js"></script>
{{end}} {{end}}
</head> </head>
<body> <body>

View File

@ -2,7 +2,7 @@
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-narrow"> <div class="column is-narrow">
<h1 class="is-size-3 title"> <h1 class="is-size-3 title">
<a href="/" rel="Inbox" class="has-text-grey-dark"> <a href="{{ .BaseURL }}/" rel="Inbox" class="has-text-grey-dark">
{{if or .Messages .SMS}} {{if or .Messages .SMS}}
📳 📳
{{else}} {{else}}

View File

@ -2,7 +2,7 @@
{{define "header_buttons"}} {{define "header_buttons"}}
<button <button
data-controller="restful" data-controller="restful"
data-restful-endpoint="/sms" data-restful-endpoint="{{ .BaseURL }}/sms"
data-restful-method="DELETE" data-restful-method="DELETE"
class="button is-danger"> class="button is-danger">
🗑️ Clear 🗑️ Clear
@ -23,10 +23,11 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ $baseURL := .BaseURL }}
{{range .Messages}} {{range .Messages}}
<tr data-controller="inbox-entry" <tr data-controller="inbox-entry"
data-action="click->outbox-entry#onClick" data-action="click->outbox-entry#onClick"
data-inbox-entry-link="./sms/{{ .ID }}"> data-inbox-entry-link="{{ $baseURL }}/sms/{{ .ID }}">
<td class="sms-from"> <td class="sms-from">
<span class="is-size-7">{{ .From }}</span> <span class="is-size-7">{{ .From }}</span>
</td> </td>
@ -38,7 +39,7 @@
</td> </td>
<td class="sms-actions"> <td class="sms-actions">
<div class="buttons is-right"> <div class="buttons is-right">
<a href="./sms/{{ .ID }}" class="button is-small is-link">👁️ See</a> <a href="{{ $baseURL }}/sms/{{ .ID }}" class="button is-small is-link">👁️ See</a>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -2,9 +2,9 @@
{{define "header_buttons"}} {{define "header_buttons"}}
<button class="button is-danger" <button class="button is-danger"
data-controller="restful" data-controller="restful"
data-restful-endpoint="./{{ .SMS.ID }}" data-restful-endpoint="{{ .BaseURL }}/sms/{{ .SMS.ID }}"
data-restful-method="DELETE" data-restful-method="DELETE"
data-restful-redirect="../"> data-restful-redirect="{{ .BaseURL }}/">
🗑️ Delete 🗑️ Delete
</button> </button>
{{end}} {{end}}

View File

@ -20,6 +20,7 @@ type HTTPConfig struct {
Address string `yaml:"address" env:"FAKESMS_HTTP_ADDRESS"` Address string `yaml:"address" env:"FAKESMS_HTTP_ADDRESS"`
TemplateDir string `yaml:"templateDir" env:"FAKESMS_HTTP_TEMPLATEDIR"` TemplateDir string `yaml:"templateDir" env:"FAKESMS_HTTP_TEMPLATEDIR"`
PublicDir string `yaml:"publicDir" env:"FAKESMS_HTTP_PUBLICDIR"` PublicDir string `yaml:"publicDir" env:"FAKESMS_HTTP_PUBLICDIR"`
BaseURL string `yaml:"baseUrl" env:"FAKESMS_HTTP_BASEURL"`
} }
type DataConfig struct { type DataConfig struct {
@ -27,8 +28,7 @@ type DataConfig struct {
} }
type PowowConfig struct { type PowowConfig struct {
APIKey string `yaml:"apiKey" env:"FAKESMS_POWOW_API_KEY"` APIKey string `yaml:"apiKey" env:"FAKESMS_POWOW_API_KEY"`
SMS []PowowSMS `yaml:"sms"`
} }
type PowowSMS struct { type PowowSMS struct {
@ -79,17 +79,6 @@ func NewDefault() *Config {
}, },
Powow: PowowConfig{ Powow: PowowConfig{
APIKey: "powow", APIKey: "powow",
SMS: []PowowSMS{
{
Name: "Powow SMS",
From: "FakeSMS",
ShortLink: false,
Content: `Bonjour %Subscriber:Firstname%,
Lorem ipsum dolor sit amet...
`,
},
},
}, },
} }
} }

View File

@ -0,0 +1,10 @@
package powow
type SMSTemplate struct {
ID int `storm:"id,increment"`
SmsName string
FromName string
Content string
ShortLink int
Language string
}

View File

@ -5,9 +5,11 @@ import (
"strconv" "strconv"
"time" "time"
"forge.cadoles.com/Cadoles/fake-sms/internal/config"
"forge.cadoles.com/Cadoles/fake-sms/internal/query" "forge.cadoles.com/Cadoles/fake-sms/internal/query"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container" "gitlab.com/wpetit/goweb/middleware/container"
"gitlab.com/wpetit/goweb/service"
"gitlab.com/wpetit/goweb/service/template" "gitlab.com/wpetit/goweb/service/template"
) )
@ -15,6 +17,7 @@ func extendTemplateData(w http.ResponseWriter, r *http.Request, data template.Da
ctn := container.Must(r.Context()) ctn := container.Must(r.Context())
data, err := template.Extend(data, data, err := template.Extend(data,
template.WithBuildInfo(w, r, ctn), template.WithBuildInfo(w, r, ctn),
withBaseURL(ctn),
) )
if err != nil { if err != nil {
@ -24,6 +27,19 @@ func extendTemplateData(w http.ResponseWriter, r *http.Request, data template.Da
return data return data
} }
func withBaseURL(ctn *service.Container) template.DataExtFunc {
return func(data template.Data) (template.Data, error) {
conf, err := config.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
data["BaseURL"] = conf.HTTP.BaseURL
return data, nil
}
}
func createOutboxQueryFromRequest(r *http.Request) (*query.GetOutboxRequest, error) { func createOutboxQueryFromRequest(r *http.Request) (*query.GetOutboxRequest, error) {
orderBy := r.URL.Query().Get("orderBy") orderBy := r.URL.Query().Get("orderBy")
reverse := r.URL.Query().Get("reverse") reverse := r.URL.Query().Get("reverse")

View File

@ -10,6 +10,8 @@ import (
"forge.cadoles.com/Cadoles/fake-sms/internal/command" "forge.cadoles.com/Cadoles/fake-sms/internal/command"
"forge.cadoles.com/Cadoles/fake-sms/internal/config" "forge.cadoles.com/Cadoles/fake-sms/internal/config"
"forge.cadoles.com/Cadoles/fake-sms/internal/model/powow"
"forge.cadoles.com/Cadoles/fake-sms/internal/storm"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs" "gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
@ -20,25 +22,32 @@ import (
type ErrorCode int type ErrorCode int
const ( const (
ErrorCodeInvalidCommand ErrorCode = 99997 ErrorCodeInvalidCommand ErrorCode = 99997
ErrorCodeAuthenticationFailure ErrorCode = 99998 ErrorCodeAuthenticationFailure ErrorCode = 99998
ErrorCodeNotEnoughPrivileges ErrorCode = 99999 ErrorCodeNotEnoughPrivileges ErrorCode = 99999
ErrorCodeMissingSMSID ErrorCode = 1 ErrorCodeTransactionSMSSendMissingSMSID ErrorCode = 1
ErrorCodeMissingMobilePhoneNumber ErrorCode = 2 ErrorCodeTransactionSMSSendMissingMobilePhoneNumber ErrorCode = 2
ErrorCodeInvalidSMSID ErrorCode = 3 ErrorCodeTransactionSMSSendInvalidSMSID ErrorCode = 3
ErrorCodeInvalidMobilePhoneNumber ErrorCode = 4 ErrorCodeTransactionSMSSendInvalidMobilePhoneNumber ErrorCode = 4
ErrorCodeInvalidCustomData ErrorCode = 5 ErrorCodeTransactionSMSSendInvalidCustomData ErrorCode = 5
ErrorCodeInvalidTimeToSend ErrorCode = 6 ErrorCodeTransactionSMSSendInvalidTimeToSend ErrorCode = 6
ErrorCodeAccountSubscribersLimitExceeded ErrorCode = 7 ErrorCodeTransactionSMSSendAccountSubscribersLimitExceeded ErrorCode = 7
ErrorCodeMobilePhoneNumberCannotBeSaved ErrorCode = 7 ErrorCodeTransactionSMSSendMobilePhoneNumberCannotBeSaved ErrorCode = 7
ErrorCodeTransactionalIDCannotBeCreated ErrorCode = 9 ErrorCodeTransactionSMSSendTransactionalIDCannotBeCreated ErrorCode = 9
ErrorCodeSMSSentLimitExceeded ErrorCode = 10 ErrorCodeTransactionSMSSendSMSSentLimitExceeded ErrorCode = 10
ErrorCodeTransactionSMSUpdateMissingSMSID ErrorCode = 1
ErrorCodeTransactionSMSUpdateInvalidSMSID ErrorCode = 3
ErrorCodeTransactionSMSUpdateInvalidFromName ErrorCode = 4
ErrorCodeTransactionSMSUpdateInvalidLanguage ErrorCode = 7
) )
type Command string type Command string
const ( const (
CommandTransactionalSMSSend = "TransactionalSms.Send" CommandTransactionalSMSSend = "TransactionalSms.Send"
CommandTransactionalSMSCreate = "TransactionalSms.Create"
CommandTransactionalSMSUpdate = "TransactionalSms.Update"
) )
type PowowRequest struct { type PowowRequest struct {
@ -103,6 +112,14 @@ func handlePowowEntrypoint(w http.ResponseWriter, r *http.Request) {
case CommandTransactionalSMSSend: case CommandTransactionalSMSSend:
handleTransactionalSMSSend(ctx, ctn, w, pr) handleTransactionalSMSSend(ctx, ctn, w, pr)
return
case CommandTransactionalSMSCreate:
handleTransactionalSMSCreate(ctx, ctn, w, pr)
return
case CommandTransactionalSMSUpdate:
handleTransactionalSMSUpdate(ctx, ctn, w, pr)
return return
default: default:
res := &PowowResponse{ res := &PowowResponse{
@ -119,45 +136,52 @@ func handlePowowEntrypoint(w http.ResponseWriter, r *http.Request) {
// Mock https://powow4.iroquois.fr/user/docs/api/#send-transactional-sms // Mock https://powow4.iroquois.fr/user/docs/api/#send-transactional-sms
func handleTransactionalSMSSend(ctx context.Context, ctn *service.Container, w http.ResponseWriter, req *PowowRequest) { func handleTransactionalSMSSend(ctx context.Context, ctn *service.Container, w http.ResponseWriter, req *PowowRequest) {
bus := cqrs.Must(ctn) bus := cqrs.Must(ctn)
conf := config.Must(ctn) db := storm.Must(ctn)
rawSMSID, exists := req.Payload["SmsID"] smsID, exists, valid := getPowowSMSID(req)
if !exists { if !exists {
sendPowowResponse(w, &PowowResponse{ sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeMissingSMSID, ErrorCode: ErrorCodeTransactionSMSSendMissingSMSID,
Success: false, Success: false,
}) })
return return
} }
smsID, ok := rawSMSID.(float64) if !valid {
if !ok {
sendPowowResponse(w, &PowowResponse{ sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeInvalidSMSID, ErrorCode: ErrorCodeTransactionSMSSendInvalidSMSID,
Success: false, Success: false,
}) })
return return
} }
if smsID < 0 || int(smsID) > len(conf.Powow.SMS)-1 { smsTmpl := &powow.SMSTemplate{}
sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeInvalidSMSID,
Success: false,
})
return if err := db.One("ID", smsID, smsTmpl); err != nil {
if errors.Is(err, storm.ErrNotFound) {
sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeTransactionSMSSendInvalidSMSID,
Success: false,
})
return
}
panic(errors.Wrap(err, "could not retrieve sms template"))
} }
customData := make(map[string]interface{}) customData := make(map[string]interface{})
var ok bool
rawCustomData, exists := req.Payload["CustomData"] rawCustomData, exists := req.Payload["CustomData"]
if exists { if exists {
customData, ok = rawCustomData.(map[string]interface{}) customData, ok = rawCustomData.(map[string]interface{})
if !ok { if !ok {
sendPowowResponse(w, &PowowResponse{ sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeInvalidCustomData, ErrorCode: ErrorCodeTransactionSMSSendInvalidCustomData,
Success: false, Success: false,
}) })
@ -165,17 +189,15 @@ func handleTransactionalSMSSend(ctx context.Context, ctn *service.Container, w h
} }
} }
sms := conf.Powow.SMS[int(smsID)] body, err := createSMSBody(smsTmpl.Content, customData)
body, err := createSMSBody(sms.Content, customData)
if err != nil { if err != nil {
panic(errors.Wrap(err, "could not generate sms body")) panic(errors.Wrap(err, "could not generate sms body"))
} }
req.Payload["_Template"] = sms req.Payload["_Template"] = smsTmpl
storeSMS := &command.StoreSMSRequest{ storeSMS := &command.StoreSMSRequest{
From: sms.From, From: smsTmpl.FromName,
Body: body, Body: body,
Recipient: req.Payload["MobilePhoneNumber"].(string), Recipient: req.Payload["MobilePhoneNumber"].(string),
Metadata: req.Payload, Metadata: req.Payload,
@ -217,6 +239,143 @@ func createSMSBody(template string, customData map[string]interface{}) (string,
return content, nil return content, nil
} }
// Mock https://powow4.iroquois.fr/user/docs/api/#create-transactional-sms
func handleTransactionalSMSCreate(ctx context.Context, ctn *service.Container, w http.ResponseWriter, req *PowowRequest) {
db := storm.Must(ctn)
smsTemplate := &powow.SMSTemplate{}
if err := db.Save(smsTemplate); err != nil {
panic(errors.Wrap(err, "could not save sms template"))
}
res := &struct {
PowowResponse
SmsID int
}{
PowowResponse: PowowResponse{
Success: true,
ErrorCode: 0,
},
SmsID: smsTemplate.ID,
}
sendPowowResponse(w, res)
}
// Mock https://powow4.iroquois.fr/user/docs/api/#update-transactional-sms
func handleTransactionalSMSUpdate(ctx context.Context, ctn *service.Container, w http.ResponseWriter, req *PowowRequest) {
smsID, exists, valid := getPowowSMSID(req)
if !exists {
sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeTransactionSMSUpdateMissingSMSID,
Success: false,
})
return
}
if !valid {
sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeTransactionSMSUpdateInvalidSMSID,
Success: false,
})
return
}
db := storm.Must(ctn)
smsTmpl := &powow.SMSTemplate{}
if err := db.One("ID", smsID, smsTmpl); err != nil {
if errors.Is(err, storm.ErrNotFound) {
sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeTransactionSMSUpdateInvalidSMSID,
Success: false,
})
return
}
panic(errors.Wrap(err, "could not retrieve sms template"))
}
rawContent, exists := req.Payload["Content"]
if exists {
content, ok := rawContent.(string)
if ok {
smsTmpl.Content = content
}
}
rawLanguage, exists := req.Payload["Language"]
if exists {
language, ok := rawLanguage.(string)
if ok {
if !contains(language, "en", "fr", "it", "es", "de", "pt", "pl", "zh") {
sendPowowResponse(w, &PowowResponse{
ErrorCode: ErrorCodeTransactionSMSUpdateInvalidLanguage,
Success: false,
})
return
}
smsTmpl.Language = language
}
}
rawFromName, exists := req.Payload["FromName"]
if exists {
fromName, ok := rawFromName.(string)
if ok {
smsTmpl.FromName = fromName
}
}
rawShortLink, exists := req.Payload["ShortLink"]
if exists {
shortLink, ok := rawShortLink.(float64)
if ok {
if shortLink == 1.0 {
smsTmpl.ShortLink = 1
} else {
smsTmpl.ShortLink = 0
}
}
}
rawSMSName, exists := req.Payload["SmsName"]
if exists {
smsName, ok := rawSMSName.(string)
if ok {
smsTmpl.SmsName = smsName
}
}
rawShortLink, exists = req.Payload["ShortLink"]
if exists {
shortLink, ok := rawShortLink.(float64)
if ok {
if shortLink == 1.0 {
smsTmpl.ShortLink = 1
} else {
smsTmpl.ShortLink = 0
}
}
}
if err := db.Save(smsTmpl); err != nil {
panic(errors.Wrap(err, "could not save sms template"))
}
sendPowowResponse(w, &PowowResponse{
ErrorCode: 0,
Success: true,
})
}
func sendPowowResponse(w http.ResponseWriter, res interface{}) { func sendPowowResponse(w http.ResponseWriter, res interface{}) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
@ -227,3 +386,17 @@ func sendPowowResponse(w http.ResponseWriter, res interface{}) {
panic(errors.WithStack(err)) panic(errors.WithStack(err))
} }
} }
func getPowowSMSID(req *PowowRequest) (smsID int, exists bool, valid bool) {
rawSMSID, exists := req.Payload["SmsID"]
if !exists {
return -1, false, false
}
smsIDFloat, ok := rawSMSID.(float64)
if !ok {
return -1, true, false
}
return int(smsIDFloat), true, true
}

11
internal/route/util.go Normal file
View File

@ -0,0 +1,11 @@
package route
func contains(search string, items ...string) bool {
for _, item := range items {
if item == search {
return true
}
}
return false
}

View File

@ -1,3 +1,34 @@
### Create transactional SMS template
# @name createSms
POST http://localhost:3000/api/v1/mock/powow
Content-Type: application/json
{
"APIKey": "powow",
"Command": "TransactionalSms.Create"
}
### Update transactional SMS template
@SmsID = {{createSms.response.body.$.SmsID}}
POST http://localhost:3000/api/v1/mock/powow
Content-Type: application/json
{
"APIKey": "powow",
"Command": "TransactionalSms.Update",
"SmsID": {{SmsID}},
"SmsName": "Defaut SMS",
"FromName": "FakeSMS",
"Content": "Bonjour %Subscriber:Firstname%,\nLorem ipsum dolor sit amet...",
"ShortLink": 0,
"Language": "fr"
}
### Send transactional SMS
POST http://localhost:3000/api/v1/mock/powow POST http://localhost:3000/api/v1/mock/powow
Content-Type: application/json Content-Type: application/json
@ -5,7 +36,7 @@ Content-Type: application/json
"APIKey": "powow", "APIKey": "powow",
"Command": "TransactionalSms.Send", "Command": "TransactionalSms.Send",
"ResponseFormat": "JSON", "ResponseFormat": "JSON",
"SmsID": 0, "SmsID": {{SmsID}},
"MobilePhoneNumber": "+33699999999", "MobilePhoneNumber": "+33699999999",
"TimeToSend": "2017-01-01 10:00:00", "TimeToSend": "2017-01-01 10:00:00",
"CustomData": { "CustomData": {