Getting More than 1 push notification using go lang cron - go

I am creating GO rest APIs. We are using AWS server.
I want to send push notification to mobile. Then I used
https://pkg.go.dev/github.com/robfig/cron (https://github.com/robfig/cron )
for creating cron job.
We are using 2 version of API, V1(old one) and V1.1(new one)
we have more than 1 environment dev,QA,preproduction,production
in our go lang code I created a cron job for sending push notification to mobile. and the function called inside main().
But we are getting 2 notification each interval.
I didn't understand why 2 one is getting at a time
I am attaching my code.
const title = "This Week’s Activity"
func NotifyWeeklyActivity(db *sql.DB, logger *zap.Logger) {
logger.Info("NotifyWeeklyActivity- start")
c := cron.New()
c.AddFunc("*/5 * * * *", func() {
lastweekTime := CurrentUTC().AddDate(0, 0, -7)
type PostCount struct {
HiveID uint64 `json:"hive_id"`
Post uint64 `json:"post"`
NotificationTopicArn null.String `json:"notification_topic_arn"`
}
var posts []PostCount
err := queries.Raw(`
select count(post_id) as post , post.hive_id as hive_id , hive.notification_topic_arn
from post
join hive on post.hive_id=hive.hive_id and hive.deleted_at is null
where post.deleted_at is null
and hive.deleted_at is null
and post.created_at between ? and ?
group by hive_id
having count(post_id)>3 ;
`, lastweekTime, CurrentUTC()).Bind(context.TODO(), db, &posts)
if err != nil {
logger.Error("error while fetching data ", zap.Error(err))
// return err
}
cfg, _ := config.GetImpart()
if cfg.Env != config.Local {
notification := NewImpartNotificationService(db, string(cfg.Env), cfg.Region, cfg.IOSNotificationARN, logger)
logger.Info("Notification- fetching complted")
for _, hive := range posts {
pushNotification := Alert{
Title: aws.String(title),
Body: aws.String(
fmt.Sprintf("Check out %d new posts in your Hive this week", hive.Post),
),
}
additionalData := NotificationData{
EventDatetime: CurrentUTC(),
HiveID: hive.HiveID,
}
Logger.Info("Notification",
zap.Any("pushNotification", pushNotification),
zap.Any("additionalData", additionalData),
zap.Any("hive", hive),
)
err = notification.NotifyTopic(context.Background(), additionalData, pushNotification, hive.NotificationTopicArn.String)
if err != nil {
logger.Error("error sending notification to topic", zap.Error(err))
}
}
}
})
c.Start()
}
func NewImpartNotificationService(db *sql.DB, stage, region, platformApplicationARN string, logger *zap.Logger) NotificationService {
//SNS not available in us-east-2
if strings.EqualFold(region, "us-east-2") {
region = "us-east-1"
}
sess, err := session.NewSession(&aws.Config{
Region: aws.String(region),
HTTPClient: NewHttpClient(10 * time.Second),
})
if err != nil {
logger.Fatal("unable to create aws session", zap.Error(err))
}
snsAppleNotificationService := &snsAppleNotificationService{
stage: stage,
Logger: logger,
SNS: sns.New(sess),
platformApplicationARN: platformApplicationARN,
db: db,
}
logger.Debug("created new NotificationService",
zap.String("stage", stage),
zap.String("arn", platformApplicationARN))
return snsAppleNotificationService
}
Why I am getting 2 notification at a time ?
How can I Solve this
func (ns *snsAppleNotificationService) NotifyTopic(ctx context.Context, data NotificationData, alert Alert, topicARN string) error {
var b []byte
var err error
if strings.TrimSpace(topicARN) == "" {
return nil
}
ns.Logger.Debug("sending push notification",
zap.Any("data", data),
zap.Any("msg", alert),
zap.String("platformEndpoint", topicARN),
zap.String("arn", ns.platformApplicationARN))
if b, err = json.Marshal(apnsMessageWrapper{
APNSData: APNSMessage{
Alert: alert,
Sound: aws.String("default"),
Data: data,
Badge: aws.Int(0),
},
}); err != nil {
return err
}
msg := awsSNSMessage{Default: *alert.Body}
msg.APNS = string(b)
msg.APNSSandbox = string(b)
if b, err = json.Marshal(msg); err != nil {
return err
}
input := &sns.PublishInput{
Message: aws.String(string(b)),
MessageStructure: aws.String("json"),
TopicArn: aws.String(topicARN),
}
// print()
_, err = ns.Publish(input)
if err != nil {
ns.Logger.Error("push-notification : After publish input",
zap.Any("topicARN", topicARN),
zap.Error(err),
)
}
return err
}
main fuction
func main() {
logger, err := zap.NewProduction()
if err != nil {
log.Fatal(err)
}
cfg, err := config.GetImpart()
if err != nil {
logger.Fatal("error parsing config", zap.Error(err))
}
if cfg == nil {
logger.Fatal("nil config")
return
}
if cfg.Debug {
gin.SetMode(gin.DebugMode)
//boil.DebugMode = true
boil.WithDebugWriter(context.TODO(), &config.ZapBoilWriter{Logger: logger})
logger, _ = zap.NewDevelopment()
if cfg.Env == config.Local || cfg.Env == config.Development {
logger.Debug("config startup", zap.Any("config", *cfg))
}
} else {
gin.SetMode(gin.ReleaseMode)
}
//init the sentry logger ,either debug
logger, err = impart.InitSentryLogger(cfg, logger, cfg.Debug)
if err != nil {
logger.Error("error on sentry init", zap.Any("error", err))
}
migrationDB, err := cfg.GetMigrationDBConnection()
if err != nil {
logger.Fatal("unable to connect to DB", zap.Error(err))
}
//Trap sigterm during migraitons
migrationsDoneChan := make(chan bool)
shutdownMigrationsChan := make(chan bool)
sigc := make(chan os.Signal, 1)
signal.Notify(sigc,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)
go func() {
select {
case <-sigc:
logger.Info("received a shutdown request during migrations, sending shutdown signal")
shutdownMigrationsChan <- true
case <-migrationsDoneChan:
logger.Info("migrations complete, no longer waiting for sig int")
return
}
}()
err = migrater.RunMigrationsUp(migrationDB, cfg.MigrationsPath, logger, shutdownMigrationsChan)
if err != nil {
logger.Fatal("error running migrations", zap.Error(err))
}
migrationsDoneChan <- true
if err := migrationDB.Close(); err != nil {
logger.Fatal("error closing migrations DB connection", zap.Error(err))
}
boil.SetLocation(time.UTC)
db, err := cfg.GetDBConnection()
if err != nil {
logger.Fatal("unable to connect to DB", zap.Error(err))
}
defer db.Close()
defer logger.Sync()
// if err := migrater.BootStrapAdminUsers(db, cfg.Env, logger); err != nil {
// logger.Fatal("unable to bootstrap user", zap.Error(err))
// }
// if err := migrater.BootStrapTopicHive(db, cfg.Env, logger); err != nil {
// logger.Fatal("unable to bootstrap user", zap.Error(err))
// }
// initiate global profanity detector
impart.InitProfanityDetector(db, logger)
impart.NotifyWeeklyActivity(db, logger)
services := setupServices(cfg, db, logger)
r := gin.New()
r.Use(CORS)
r.Use(secure.Secure(secure.Options{
//AllowedHosts: []string{"*"},
// AllowedHosts: []string{"localhost:3000", "ssl.example.com"},
//SSLRedirect: true,
// SSLHost: "*",
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
STSIncludeSubdomains: true,
FrameDeny: true,
ContentTypeNosniff: true,
BrowserXssFilter: true,
ContentSecurityPolicy: "default-src 'self'",
}))
r.RedirectTrailingSlash = true
r.Use(ginzap.RecoveryWithZap(logger, true)) // panics don't stop server
r.Use(ginzap.Ginzap(logger, time.RFC3339, true)) // logs all requests
r.NoRoute(noRouteFunc)
r.GET("/ping", func(ctx *gin.Context) {
_, err := dbmodels.Pings(dbmodels.PingWhere.Ok.EQ(true)).One(ctx, db)
if err != nil {
ctx.AbortWithStatus(http.StatusInternalServerError)
}
ctx.String(http.StatusOK, "pong")
})
var v1Route string
var v2Route string
if cfg.Env == config.Production || cfg.Env == config.Local {
v1Route = "v1"
v2Route = "v1.1"
} else {
v1Route = fmt.Sprintf("%s/v1", cfg.Env)
v2Route = fmt.Sprintf("%s/v1.1", cfg.Env)
}
err = mailchimp.SetKey(impart.MailChimpApiKey)
if err != nil {
logger.Info("Error connecting Mailchimp", zap.Error(err),
zap.Any("MailchimpApikey", cfg.MailchimpApikey))
}
v1 := r.Group(v1Route)
setRouter(v1, services, logger, db)
v2 := r.Group(v2Route)
setRouter(v2, services, logger, db)
server := cfg.GetHttpServer()
server.Handler = r
logger.Info("Impart backend started.", zap.Int("port", cfg.Port), zap.String("env", string(cfg.Env)))
if err := graceful.Graceful(server.ListenAndServe, server.Shutdown); err != nil {
logger.Fatal("error serving", zap.Error(err))
}
logger.Info("done serving")
}
publish
// See also, https://docs.aws.amazon.com/goto/WebAPI/sns-2010-03-31/Publish
func (c *SNS) Publish(input *PublishInput) (*PublishOutput, error) {
req, out := c.PublishRequest(input)
return out, req.Send()
}

Related

I get an error in golang test and would like to know how to improve it

sta9_test.go:124: Cannot convert id to numeric, got = id
sta9_test.go:124: Cannot convert id to numericCannot convert id to numeric, got = id
sta9_test.go:145: It is a value we do not expect.
map[string]any{
- "UpdatedAt": string("2022-10-27T15:19:10Z"),
- "createdAenter code heret": string("2022-10-27T15:19:10Z"),
"description": string(""),}
func TestStation9(t *testing.T) {
dbPath := "./temp_test.db"
if err := os.Setenv("DB_PATH", dbPath); err != nil {
t.Error("dbPathのセットに失敗しました。", err)
return
}
t.Cleanup(func() {
if err := os.Remove(dbPath); err != nil {
t.Errorf("テスト用のDBファイルの削除に失敗しました: %v", err)
return
}
})
todoDB, err := db.NewDB(dbPath)
if err != nil {
t.Error("DBの作成に失敗しました。", err)
return
}
defer func(todoDB *sql.DB) {
err := todoDB.Close()
if err != nil {
t.Error("DBのクローズに失敗しました.", err)
}
}(todoDB)
r := router.NewRouter(todoDB)
srv := httptest.NewServer(r)
defer srv.Close()
testcases := map[string]struct {
Subject string
Description string
WantHTTPStatusCode int
}{
"Subject is empty": {
WantHTTPStatusCode: http.StatusBadRequest,
},
"Description is empty": {
Subject: "todo subject",
WantHTTPStatusCode: http.StatusOK,
},
"Subject and Description is not empty": {
Subject: "todo subject",
Description: "todo description",
WantHTTPStatusCode: http.StatusOK,
},
}
for name, tc := range testcases {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
resp, err := http.Post(srv.URL+"/todos", "application/json",
bytes.NewBufferString(fmt.Sprintf(`{"subject":"%s","description":"%s"}`, tc.Subject, tc.Description)))
if err != nil {
t.Error("リクエストの送信に失敗しました。", err)
return
}
defer func() {
if err := resp.Body.Close(); err != nil {
t.Error("レスポンスのクローズに失敗しました。", err)
return
}
}()
if resp.StatusCode != tc.WantHTTPStatusCode {
t.Errorf("期待していない HTTP status code です, got = %d, want = %d", resp.StatusCode, tc.WantHTTPStatusCode)
return
}
if tc.WantHTTPStatusCode != http.StatusOK {
return
}
var m map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
t.Error("レスポンスのデコードに失敗しました。", err)
return
}
v, ok := m["todo"]
if !ok {
t.Error("レスポンスの中にtodoがありません。")
return
}
got, ok := v.(map[string]interface{})
if !ok {
t.Error("レスポンスの中のtodoがmapではありません。")
return
}
want := map[string]interface{}{
"subject": tc.Subject,
"description": tc.Description,
}
now := time.Now().UTC()
fmt.Println(got)
fmt.Println(want)
diff := cmp.Diff(got, want, cmpopts.IgnoreMapEntries(func(k string, v interface{}) bool {
switch k {
case "id":
if vv, _ := v.(float64); vv == 0 {
t.Errorf("id を数値に変換できません, got = %s", k)
}
return true
case "created_at", "updated_at":
vv, ok := v.(string)
if !ok {
t.Errorf("日付が文字列に変換できません, got = %+v", k)
return true
}
if tt, err := time.Parse(time.RFC3339, vv); err != nil {
t.Errorf("日付が期待しているフォーマットではありません, got = %s", k)
} else if now.Before(tt) {
t.Errorf("日付が未来の日付になっています, got = %s", tt)
}
return true
}
return false
}))
if diff != "" {
t.Error("期待していない値です\n", diff)
}
})
}
}
type TODOHandler struct {
svc *service.TODOService
}
// NewTODOHandler returns TODOHandler based http.Handler.
func NewTODOHandler(svc *service.TODOService) *TODOHandler {
return &TODOHandler{
svc: svc,
}
}
func (h *TODOHandler)ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost{
var todo model.CreateTODORequest
if err := json.NewDecoder(r.Body).Decode(&todo); err != nil {
log.Fatal(err)
}
if todo.Subject == ""{
w.WriteHeader(400)
}else{
createdTodo, _ := h.Create(r.Context(),&todo)
e := json.NewEncoder(w)
if err:= e.Encode(createdTodo); err != nil{
log.Fatal("ListenAndServe:", err)
}
}
}
}
// Create handles the endpoint that creates the TODO.
func (h *TODOHandler) Create(ctx context.Context, req *model.CreateTODORequest) (*model.CreateTODOResponse, error) {
createtodo, err := h.svc.CreateTODO(ctx, req.Subject, req.Description)
if err != nil{
log.Fatal(err)
}
return &model.CreateTODOResponse{TODO: *createtodo}, nil
}
type TODOService struct {
db *sql.DB
}
// NewTODOService returns new TODOService.
func NewTODOService(db *sql.DB) *TODOService {
return &TODOService{
db: db,
}
}
// CreateTODO creates a TODO on DB.
func (s *TODOService) CreateTODO(ctx context.Context, subject, description string) (*model.TODO, error) {
const (
insert = `INSERT INTO todos(subject, description) VALUES(?, ?)`
confirm = `SELECT subject, description, created_at, updated_at FROM todos WHERE id = ?`
)
var todo model.TODO
if subject == ""{
msg := "subjectがありません"
return &todo,fmt.Errorf("err %s", msg)
}
stmt,err := s.db.PrepareContext(ctx,insert)
if err != nil{
log.Fatal("server/todo.go s.db.ExecContext(ctx,insert,subject,description)",err)
}
defer stmt.Close()
res, err := stmt.ExecContext(ctx, subject,description)
if err != nil {
return nil, err
}
insert_id,err := res.LastInsertId()
if err != nil{
log.Fatal("server/todo.go stmt.RowsAffected()",err)
}
err = s.db.QueryRowContext(ctx,confirm,insert_id).Scan(&todo.Subject,&todo.Description,&todo.CreatedAt,&todo.UpdatedAt)
if err != nil{
log.Fatal("server/todo.go s.db.QueryRowContext(ctx,confirm,insert_id).Scan(&todo.Subject,&todo.Description,&todo.CreatedAt,&todo.UpdatedAt)",err)
}
return &todo, err
}
I am building a TODO application with TDD and have a question. I get the error at the top. What kind of error is this and how can I improve it?

Golang Redis websocket handler

I need help with Golang websocket. I'm using Fiber with websocket and redis.
Here is the code:
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/go-redis/redis/v8"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/websocket/v2"
"log"
"test4/controllers"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
var ctx = context.Background()
var redisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
func TestSocket() fiber.Handler {
socket := websocket.New(func(c *websocket.Conn) {
go deliverMessages(c)
var (
msg []byte
err error
)
for {
if _, msg, err = c.ReadMessage(); err != nil {
log.Println("read:", err)
break
}
if err := redisClient.Publish(ctx, "chat", msg).Err(); err != nil {
log.Println("publish:", err)
break
}
}
})
return socket
}
func deliverMessages(c *websocket.Conn) {
subscriber := redisClient.Subscribe(ctx, "chat")
user := User{}
for {
msg, err := subscriber.ReceiveMessage(ctx)
if err != nil {
log.Println("subscriber:", err)
panic(err)
}
if err := json.Unmarshal([]byte(msg.Payload), &user); err != nil {
log.Println("Unmarshal:", err)
panic(err)
}
text := []byte(fmt.Sprintf("{\"name\":\"%s\", \"email\":\"%s\"}", user.Name, user.Email))
if err = c.WriteMessage(websocket.TextMessage, text); err != nil {
log.Println("write:", err)
break
}
}
}
func main() {
app := fiber.New(fiber.Config{
Prefork: true,
CaseSensitive: true,
StrictRouting: true,
DisableStartupMessage: true,
ServerHeader: "Test v3",
})
app.Get("/", controllers.Home)
app.Get("/ws", TestSocket())
log.Fatal(app.Listen("0.0.0.0:3000"))
}
How to produce the error:
Install Redis and run go run main.go
Now open http://127.0.0.1:3000/ in two tabs
click open on both tabs, and then you will see OPEN on right side of browser
click send on both tabs and you will get SEND and RESPONSE
Now close one tab and on go program terminal you will see error (see attached screenshot)
Now publish data to chat channel on redis-cli
Here is the error I am getting:
I think this is nil pointer websocket.Conn issue.
When close websocket connection, goroutine's c *websocket.Conn is loose data.
Pointer point nil.
solution
use channel, use local redisClient var
func TestSocket() fiber.Handler {
socket := websocket.New(func(c *websocket.Conn) {
var redisClient = redis.NewClient(&redis.Options{ // <-- use local redisClient var
Addr: "localhost:6379",
})
go deliverMessages(c)
var (
msg []byte
err error
)
defer func() {
redisClient.Close() // <-- then close, when websocket connection close
quitSubscribeGoRutine <- true // <-- change true, when websocket connection close
}()
for {
if _, msg, err = c.ReadMessage(); err != nil {
log.Println("read:", err)
...
func deliverMessages(c *websocket.Conn) {
subscriber := redisClient.Subscribe(ctx, "chat")
user := User{}
for {
select {
case <-quitSubscribeGoRutine: // <-- exit goroutine, when channel is true
return
default:
msg, err := subscriber.ReceiveMessage(ctx) // <-- exit goroutine, when redisClient close
if err != nil {
log.Println("subscriber:", err)
break // <-- use break instead of panic
}
if err := json.Unmarshal([]byte(msg.Payload), &user); err != nil {
log.Println("Unmarshal:", err)
panic(err)
}
text := []byte(fmt.Sprintf("{\"name\":\"%s\", \"email\":\"%s\"}", user.Name, user.Email))
if err = c.WriteMessage(websocket.TextMessage, text); err != nil {
log.Println("write:", err)
break
}
}
}
}

Unit Test: SMTP mock server, for testing SMTP send email

I have test email logic, to send email, that is working fine.
Now to unit test that, I copied mock SMTP server code from https://golang.org/src/net/smtp/smtp_test.go, line 639 function => TestSendMailWithAuth
Mail code that works :
Types used
type Email struct {
Subject string
Body string
TemplateFile string
TemplateData interface{}
}
type pluginInput struct{
SMTPServer string
Username string
Password string
From string
To string
Cc string
}
email := &Email{
Subject: "SendMail test",
Body: `<!DOCTYPE html>
<html lang="en">
<head>
<title>IDP Execution Summary Email</title>
</head>
<body>test email</body>
</html>`,
}
Code that sends email
func (email *Email) SendMail(pluginInput *PluginInput) error {
// Connect to the remote SMTP server.
smtpClient, err := smtp.Dial(pluginInput.SMTPServer)
if err != nil {
logger.Error(err)
return err
}
//smtpServerHost
smtpServerHost, _, err := net.SplitHostPort(pluginInput.SMTPServer)
//start tls with no certificate check
if ok, _ := smtpClient.Extension("STARTTLS"); ok {
// #nosec G402
config := &tls.Config{ServerName: smtpServerHost, InsecureSkipVerify: true}
if err = smtpClient.StartTLS(config); err != nil {
return err
}
}
//set smtp client auth
if ok, authMechanism := smtpClient.Extension("AUTH"); ok {
userNameWithoutDomain := strings.Split(pluginInput.Username, "#")[0]
switch authMechanism {
case ``:
err = smtpClient.Auth(nil)
case `LOGIN`:
err = smtpClient.Auth(LoginAuth(userNameWithoutDomain, pluginInput.Password))
case `CRAM-MD5`:
err = smtpClient.Auth(smtp.CRAMMD5Auth(userNameWithoutDomain, pluginInput.Password))
case `PLAIN`:
err = smtpClient.Auth(smtp.PlainAuth("", userNameWithoutDomain, pluginInput.Password, smtpServerHost))
default:
err = smtpClient.Auth(smtp.PlainAuth("", userNameWithoutDomain, pluginInput.Password, smtpServerHost))
}
if err != nil {
return err
}
}
// From
if err = smtpClient.Mail(pluginInput.Username); err != nil {
logger.Error(err)
return err
}
// To & Cc
toArr := strings.Split(pluginInput.To, ",")
ccArr := strings.Split(pluginInput.Cc, ",")
toArr = append(toArr, ccArr...)
for _, addr := range toArr {
if err = smtpClient.Rcpt(addr); err != nil {
return err
}
}
//body
msg := "To: " + pluginInput.To + "\r\n" +
"Cc: " + pluginInput.Cc + "\r\n" +
"Subject: " + email.Subject + "\r\n" +
mIMEHeaders + "\r\n" + email.Body
// send Data command tp smtp server
smtpWriterCloser, err := smtpClient.Data()
if err != nil {
return err
}
_, err = fmt.Fprintf(smtpWriterCloser, msg)
if err != nil {
return err
}
if err = smtpWriterCloser.Close(); err != nil {
return err
}
//send Quit command to SMTP server
if err = smtpClient.Quit(); err != nil {
return err
}
return nil
}
Unit test code for this:
func TestEmail_SendMail(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to create listener: %v", err)
}
defer l.Close()
errCh := make(chan error)
go func() {
defer close(errCh)
conn, err := l.Accept()
if err != nil {
errCh <- fmt.Errorf("Accept: %v", err)
return
}
defer conn.Close()
tc := textproto.NewConn(conn)
tc.PrintfLine("220 hello world")
msg, err := tc.ReadLine()
if err != nil {
errCh <- fmt.Errorf("ReadLine error: %v", err)
return
}
const wantMsg = "EHLO localhost"
if msg != wantMsg {
errCh <- fmt.Errorf("unexpected response %q; want %q", msg, wantMsg)
return
}
err = tc.PrintfLine("250 mx.google.com at your service")
if err != nil {
errCh <- fmt.Errorf("PrintfLine: %v", err)
return
}
}()
type fields struct {
Subject string
Body string
TemplateFile string
TemplateData interface{}
}
type args struct {
pluginInput *PluginInput
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "without tls email send testing",
fields: fields{
Subject: "SendMail test",
Body: `<!DOCTYPE html>
<html lang="en">
<head>
<title>IDP Execution Summary Email</title>
</head>
<body>test email</body>
</html>`,
},
args: args{&PluginInput{
SMTPServer: l.Addr().String(),
Username: "test#example.com",
To: "other#example.com",
Cc: "another#example.com",
}},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
email := &Email{
Subject: tt.fields.Subject,
Body: tt.fields.Body,
TemplateFile: tt.fields.TemplateFile,
TemplateData: tt.fields.TemplateData,
}
if err := email.SendMail(tt.args.pluginInput); (err != nil) != tt.wantErr {
log.Print(err)
t.Errorf("Email.SendMail() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
err = <-errCh
if err != nil {
t.Fatalf("server error: %v", err)
}
}
After running unit test it is giving "EOF" error.
I'm not very sure about mock server, because it is not having switches where based on client request it sends response.
It happened because your implementation doesn't know, when it should close active connection.
Take a look on smtpmock package: https://github.com/mocktools/go-smtp-mock. This mock server is already designed for your case or even more :)

Create a service as autorun

OS: windows/7/8/8.1/10 32bit
I have one question. How to create a service that would work like autorun?
Most applications install themselves in autorun through the registry or through C:\Users\Anon\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup. But there are those that are installing through the services, or rather as a service.
I have a code:
package main
import (
"fmt"
"strings"
"time"
"syscall"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
"golang.org/x/sys/windows/svc/debug"
"log"
"os"
"path/filepath"
"golang.org/x/sys/windows/svc/eventlog"
)
var elog debug.Log
type myservice struct{}
func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
changes <- svc.Status{State: svc.StartPending}
fasttick := time.Tick(500 * time.Millisecond)
slowtick := time.Tick(2 * time.Second)
tick := fasttick
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
elog.Info(1, strings.Join(args, "-"))
loop:
for {
select {
case <-tick:
beep()
elog.Info(1, "beep")
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
changes <- c.CurrentStatus
// Testing deadlock from https://code.google.com/p/winsvc/issues/detail?id=4
time.Sleep(100 * time.Millisecond)
changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
break loop
case svc.Pause:
changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
tick = slowtick
case svc.Continue:
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
tick = fasttick
default:
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
}
}
}
changes <- svc.Status{State: svc.StopPending}
return
}
func runService(name string, isDebug bool) {
var err error
if isDebug {
elog = debug.New(name)
} else {
elog, err = eventlog.Open(name)
if err != nil {
return
}
}
defer elog.Close()
elog.Info(1, fmt.Sprintf("starting %s service", name))
run := svc.Run
if isDebug {
run = debug.Run
}
err = run(name, &myservice{})
if err != nil {
elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err))
return
}
elog.Info(1, fmt.Sprintf("%s service stopped", name))
}
func startService(name string) error {
m, err := mgr.Connect()
if err != nil {
return err
}
defer m.Disconnect()
s, err := m.OpenService(name)
if err != nil {
return fmt.Errorf("could not access service: %v", err)
}
defer s.Close()
err = s.Start("is", "auto-started")
if err != nil {
return fmt.Errorf("could not start service: %v", err)
}
return nil
}
func controlService(name string, c svc.Cmd, to svc.State) error {
m, err := mgr.Connect()
if err != nil {
return err
}
defer m.Disconnect()
s, err := m.OpenService(name)
if err != nil {
return fmt.Errorf("could not access service: %v", err)
}
defer s.Close()
status, err := s.Control(c)
if err != nil {
return fmt.Errorf("could not send control=%d: %v", c, err)
}
timeout := time.Now().Add(10 * time.Second)
for status.State != to {
if timeout.Before(time.Now()) {
return fmt.Errorf("timeout waiting for service to go to state=%d", to)
}
time.Sleep(300 * time.Millisecond)
status, err = s.Query()
if err != nil {
return fmt.Errorf("could not retrieve service status: %v", err)
}
}
return nil
}
func main() {
const svcName = "Best Service"
isIntSess, err := svc.IsAnInteractiveSession()
if err != nil {
log.Fatalf("failed to determine if we are running in an interactive session: %v", err)
}
if !isIntSess {
runService(svcName, false)
return
}
/*err = controlService(svcName, svc.Stop, svc.Stopped)
err = removeService(svcName)*/
err = installService(svcName, "Best Service")
runService(svcName, true)
if err != nil {
log.Fatalf("failed to %s: %v", svcName, err)
}
return
}
func exePath() (string, error) {
prog := os.Args[0]
p, err := filepath.Abs(prog)
if err != nil {
return "", err
}
fi, err := os.Stat(p)
if err == nil {
if !fi.Mode().IsDir() {
return p, nil
}
err = fmt.Errorf("%s is directory", p)
}
if filepath.Ext(p) == "" {
p += ".exe"
fi, err := os.Stat(p)
if err == nil {
if !fi.Mode().IsDir() {
return p, nil
}
err = fmt.Errorf("%s is directory", p)
}
}
return "", err
}
func installService(name, desc string) error {
exepath, err := exePath()
if err != nil {
return err
}
m, err := mgr.Connect()
if err != nil {
return err
}
defer m.Disconnect()
s, err := m.OpenService(name)
if err == nil {
s.Close()
return fmt.Errorf("service %s already exists", name)
}
s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: desc, Description: "BB service"}, "is", "auto-started")
if err != nil {
return err
}
defer s.Close()
err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info)
if err != nil {
s.Delete()
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
}
return nil
}
func removeService(name string) error {
m, err := mgr.Connect()
if err != nil {
return err
}
defer m.Disconnect()
s, err := m.OpenService(name)
if err != nil {
return fmt.Errorf("service %s is not installed", name)
}
defer s.Close()
err = s.Delete()
if err != nil {
return err
}
err = eventlog.Remove(name)
if err != nil {
return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
}
return nil
}
var (
beepFunc = syscall.MustLoadDLL("user32.dll").MustFindProc("MessageBeep")
)
func beep() {
beepFunc.Call(0xffffffff)
}
Application is installed and every time I exit the application the service stops. I need that even after restarting the PC the service worked and the application started. How can I do it?
maybe it's not actual but during the creation of the service you should extend and pass Config
mgr.Config{DisplayName: desc, StartType: mgr.StartAutomatic}
like here:
s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: desc, StartType: mgr.StartAutomatic}, "is", "auto-started")
if err != nil {
return err
}
Here you can find all necessary constants and functions:
https://github.com/golang/sys/blob/master/windows/svc/mgr/config.go
On Windows 10 go to Task Scheduler > Task Scheduler Library > Create Basic Task > Trigger: when the computer starts > Action: Start a program.
When running the task, use the following user account: SYSTEM.
There is a package in the standard library that does this:
https://godoc.org/golang.org/x/sys/windows/svc
example: https://github.com/golang/sys/tree/master/windows/svc/example

Golang http: multiple response.WriteHeader calls

These days I was working on send message via websoket,using Beego framework.
but meet the wrong message http: multiple response.WriteHeader calls
Where is the problem?
Any tips would be great!
func (this *WsController) Get() {
fmt.Println("connected")
handler(this.Ctx.ResponseWriter, this.Ctx.Request, this);
conn, err := upgrader.Upgrade(this.Ctx.ResponseWriter, this.Ctx.Request, nil)
if _, ok := err.(websocket.HandshakeError); ok {
http.Error(this.Ctx.ResponseWriter, "Not a websocket handshake", 400)
return
} else if err != nil {
return
}
fmt.Println("connected")
connection := consumer.New(beego.AppConfig.String("LoggregatorAddress"), &tls.Config{InsecureSkipVerify: true}, nil)
fmt.Println("===== Tailing messages")
msgChan, err := connection.Tail(this.Ctx.Input.Param(":appGuid"), this.Ctx.Input.Param(":token"))
if err != nil {
fmt.Printf("===== Error tailing: %v\n", err)
} else {
for msg := range msgChan {
// if closeRealTimeLogFlag{
// consumer.Close()
// break
// }
if err = conn.WriteMessage(websocket.TextMessage, msg.Message); err != nil {
fmt.Println(err)
}
fmt.Printf("%v \n", msg)
}
}
}
because you write more than statusCode

Resources