func TestBackend(t *testing.T) { conf := getConfig() devEUI := lorawan.EUI64{1, 1, 1, 1, 1, 1, 1, 1} appEUI := lorawan.EUI64{2, 2, 2, 2, 2, 2, 2, 2} Convey("Given a MQTT client and Redis database", t, func() { opts := mqtt.NewClientOptions().AddBroker(conf.Server).SetUsername(conf.Username).SetPassword(conf.Password) c := mqtt.NewClient(opts) token := c.Connect() token.Wait() So(token.Error(), ShouldBeNil) p := loraserver.NewRedisPool(conf.RedisURL) Convey("Given a new Backend", func() { backend, err := NewBackend(p, conf.Server, conf.Username, conf.Password) So(err, ShouldBeNil) defer backend.Close() time.Sleep(time.Millisecond * 100) // give the backend some time to subscribe Convey("Given the MQTT client is subscribed to application/+/node/+/rxinfo", func() { rxInfoPLChan := make(chan models.RXInfoPayload) token := c.Subscribe("application/+/node/+/rxinfo", 0, func(c mqtt.Client, msg mqtt.Message) { var rxInfoPayload models.RXInfoPayload if err := json.Unmarshal(msg.Payload(), &rxInfoPayload); err != nil { t.Fatal(err) } rxInfoPLChan <- rxInfoPayload }) token.Wait() So(token.Error(), ShouldBeNil) Convey("When sending a RXInfoPayload from the backend", func() { pl := models.RXInfoPayload{ RXInfo: []models.RXInfo{ {Time: time.Now().UTC()}, }, } So(backend.SendRXInfoPayload(appEUI, devEUI, pl), ShouldBeNil) Convey("Then the same packet was received by the MQTT client", func() { pl2 := <-rxInfoPLChan So(pl2, ShouldResemble, pl) }) }) }) Convey("Given the MQTT client is subscribed to application/+/node/+/mac/rx", func() { macChan := make(chan models.MACPayload) token := c.Subscribe("application/+/node/+/mac/rx", 0, func(c mqtt.Client, msg mqtt.Message) { var mac models.MACPayload if err := json.Unmarshal(msg.Payload(), &mac); err != nil { panic(err) } macChan <- mac }) token.Wait() So(token.Error(), ShouldBeNil) Convey("When sending a MACCommand from the backend", func() { mac := models.MACPayload{ MACCommand: []byte{1, 2, 3}, } So(backend.SendMACPayload(appEUI, devEUI, mac), ShouldBeNil) Convey("Then the same MACCommand was received by the MQTT client", func() { mac2 := <-macChan So(mac2, ShouldResemble, mac) }) }) }) Convey("Given the MQTT client is subscribed to application/+/node/+/mac/error", func() { errChan := make(chan models.ErrorPayload) token := c.Subscribe("application/+/node/+/mac/error", 0, func(c mqtt.Client, msg mqtt.Message) { var pl models.ErrorPayload if err := json.Unmarshal(msg.Payload(), &pl); err != nil { panic(err) } errChan <- pl }) token.Wait() So(token.Error(), ShouldBeNil) Convey("When sending an ErrorPayload from the backend", func() { pl := models.ErrorPayload{ Message: "boom boom!", } So(backend.SendErrorPayload(appEUI, devEUI, pl), ShouldBeNil) Convey("Then the same ErrorPayload was received by the MQTT client", func() { pl2 := <-errChan So(pl2, ShouldResemble, pl) }) }) }) Convey("Given the MQTT client publishes a MAC command to application/01010101010101/node/0202020202020202/mac/tx", func() { topic := "application/01010101010101/node/0202020202020202/mac/tx" mac := models.MACPayload{ DevEUI: [8]byte{2, 2, 2, 2, 2, 2, 2, 2}, Reference: "abc123", MACCommand: []byte{1, 2, 3}, } b, err := json.Marshal(mac) So(err, ShouldBeNil) token := c.Publish(topic, 0, false, b) token.Wait() So(token.Error(), ShouldBeNil) Convey("The same MAC command is received by the backend", func() { p := <-backend.TXMACPayloadChan() So(p, ShouldResemble, mac) Convey("When the topic DevEUI does noet match the DevEUI in the message", func() { token := c.Publish("application/01010101010101/node/0303030303030303/mac/tx", 0, false, b) token.Wait() So(token.Error(), ShouldBeNil) Convey("Then the command is discarded", func() { var received bool select { case <-backend.TXMACPayloadChan(): received = true case <-time.After(time.Millisecond * 100): // nothing to do } So(received, ShouldBeFalse) }) }) }) }) }) }) }
func run(c *cli.Context) { // parse the NetID var netID lorawan.NetID if err := netID.UnmarshalText([]byte(c.String("net-id"))); err != nil { log.Fatalf("NetID parse error: %s", err) } // get the band config if c.String("band") == "" { log.Fatalf("--band is undefined, valid options are: %s", strings.Join(bands, ", ")) } bandConfig, err := band.GetConfig(band.Name(c.String("band"))) if err != nil { log.Fatal(err) } loraserver.Band = bandConfig log.WithFields(log.Fields{ "version": version, "net_id": netID.String(), "band": c.String("band"), }).Info("starting LoRa Server") // connect to the database log.Info("connecting to postgresql") db, err := loraserver.OpenDatabase(c.String("postgres-dsn")) if err != nil { log.Fatalf("database connection error: %s", err) } // setup redis pool log.Info("setup redis connection pool") rp := loraserver.NewRedisPool(c.String("redis-url")) // setup gateway backend gw, err := gateway.NewBackend(c.String("gw-mqtt-server"), c.String("gw-mqtt-username"), c.String("gw-mqtt-password")) if err != nil { log.Fatalf("gateway-backend setup failed: %s", err) } // setup application backend app, err := application.NewBackend(rp, c.String("app-mqtt-server"), c.String("app-mqtt-username"), c.String("app-mqtt-password")) if err != nil { log.Fatalf("application-backend setup failed: %s", err) } // auto-migrate the database if c.Bool("db-automigrate") { log.Info("applying database migrations") m := &migrate.AssetMigrationSource{ Asset: migrations.Asset, AssetDir: migrations.AssetDir, Dir: "", } n, err := migrate.Exec(db.DB, "postgres", m, migrate.Up) if err != nil { log.Fatalf("applying migrations failed: %s", err) } log.WithField("count", n).Info("migrations applied") } ctx := loraserver.Context{ DB: db, RedisPool: rp, Gateway: gw, Application: app, NetID: netID, } // start the loraserver server := loraserver.NewServer(ctx) if err := server.Start(); err != nil { log.Fatal(err) } // setup json-rpc api handler apiHandler, err := loraserver.NewJSONRPCHandler( loraserver.NewApplicationAPI(ctx), loraserver.NewNodeAPI(ctx), loraserver.NewNodeSessionAPI(ctx), loraserver.NewChannelListAPI(ctx), loraserver.NewChannelAPI(ctx), ) if err != nil { log.Fatal(err) } log.WithField("path", "/rpc").Info("registering json-rpc handler") http.Handle("/rpc", apiHandler) // setup static file server (for the gui) log.WithField("path", "/").Info("registering gui handler") http.Handle("/", http.FileServer(&assetfs.AssetFS{ Asset: static.Asset, AssetDir: static.AssetDir, AssetInfo: static.AssetInfo, Prefix: "", })) // start the http server go func() { log.WithField("bind", c.String("http-bind")).Info("starting http server") log.Fatal(http.ListenAndServe(c.String("http-bind"), nil)) }() sigChan := make(chan os.Signal) exitChan := make(chan struct{}) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) log.WithField("signal", <-sigChan).Info("signal received") go func() { log.Warning("stopping loraserver") if err := server.Stop(); err != nil { log.Fatal(err) } exitChan <- struct{}{} }() select { case <-exitChan: case s := <-sigChan: log.WithField("signal", s).Info("signal received, stopping immediately") } }