func TestNodeSessionAPI(t *testing.T) {
	conf := common.GetTestConfig()

	Convey("Given a clean database and api instance", t, func() {
		db, err := storage.OpenDatabase(conf.PostgresDSN)
		So(err, ShouldBeNil)
		common.MustResetDB(db)

		p := storage.NewRedisPool(conf.RedisURL)
		common.MustFlushRedis(p)

		lsCtx := loraserver.Context{DB: db, RedisPool: p, NetID: [3]byte{1, 2, 3}}
		ctx := context.Background()
		api := NewNodeSessionAPI(lsCtx)

		Convey("Given an application and node are created (fk constraints)", func() {
			app := models.Application{
				AppEUI: [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
				Name:   "test app",
			}
			So(storage.CreateApplication(db, app), ShouldBeNil)

			node := models.Node{
				DevEUI:        [8]byte{8, 7, 6, 5, 4, 3, 2, 1},
				AppEUI:        [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
				AppKey:        [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
				UsedDevNonces: [][2]byte{},
			}
			So(storage.CreateNode(db, node), ShouldBeNil)

			Convey("When creating a node-session", func() {
				_, err := api.Create(ctx, &pb.CreateNodeSessionRequest{
					DevAddr:     "06020304",
					DevEUI:      node.DevEUI.String(),
					AppEUI:      node.AppEUI.String(),
					AppSKey:     node.AppKey.String(),
					NwkSKey:     node.AppKey.String(),
					FCntUp:      10,
					FCntDown:    11,
					RxDelay:     1,
					Rx1DROffset: 2,
					CFList: []uint32{
						868700000,
					},
				})
				So(err, ShouldBeNil)

				Convey("Then it can be retrieved by DevAddr", func() {
					resp, err := api.Get(ctx, &pb.GetNodeSessionRequest{DevAddr: "06020304"})
					So(err, ShouldBeNil)
					So(resp, ShouldResemble, &pb.GetNodeSessionResponse{
						DevAddr:     "06020304",
						DevEUI:      node.DevEUI.String(),
						AppEUI:      node.AppEUI.String(),
						AppSKey:     node.AppKey.String(),
						NwkSKey:     node.AppKey.String(),
						FCntUp:      10,
						FCntDown:    11,
						RxDelay:     1,
						Rx1DROffset: 2,
						CFList: []uint32{
							868700000,
							0,
							0,
							0,
							0,
						},
					})
				})

				Convey("Then it can be retrieved by DevEUI", func() {
					resp, err := api.GetByDevEUI(ctx, &pb.GetNodeSessionByDevEUIRequest{DevEUI: node.DevEUI.String()})
					So(err, ShouldBeNil)
					So(resp, ShouldResemble, &pb.GetNodeSessionResponse{
						DevAddr:     "06020304",
						DevEUI:      node.DevEUI.String(),
						AppEUI:      node.AppEUI.String(),
						AppSKey:     node.AppKey.String(),
						NwkSKey:     node.AppKey.String(),
						FCntUp:      10,
						FCntDown:    11,
						RxDelay:     1,
						Rx1DROffset: 2,
						CFList: []uint32{
							868700000,
							0,
							0,
							0,
							0,
						},
					})
				})

				Convey("When updating the node-session", func() {
					_, err := api.Update(ctx, &pb.UpdateNodeSessionRequest{
						DevAddr:     "06020304",
						DevEUI:      node.DevEUI.String(),
						AppEUI:      node.AppEUI.String(),
						AppSKey:     node.AppKey.String(),
						NwkSKey:     node.AppKey.String(),
						FCntUp:      20,
						FCntDown:    22,
						RxDelay:     10,
						Rx1DROffset: 20,
						CFList: []uint32{
							868700000,
							868800000,
						},
					})
					So(err, ShouldBeNil)

					Convey("Then the node-session has been updated", func() {
						resp, err := api.Get(ctx, &pb.GetNodeSessionRequest{DevAddr: "06020304"})
						So(err, ShouldBeNil)
						So(resp, ShouldResemble, &pb.GetNodeSessionResponse{
							DevAddr:     "06020304",
							DevEUI:      node.DevEUI.String(),
							AppEUI:      node.AppEUI.String(),
							AppSKey:     node.AppKey.String(),
							NwkSKey:     node.AppKey.String(),
							FCntUp:      20,
							FCntDown:    22,
							RxDelay:     10,
							Rx1DROffset: 20,
							CFList: []uint32{
								868700000,
								868800000,
								0,
								0,
								0,
							},
						})
					})
				})

				Convey("When deleting the node-session", func() {
					_, err := api.Delete(ctx, &pb.DeleteNodeSessionRequest{DevAddr: "06020304"})
					So(err, ShouldBeNil)

					Convey("Then the node-session has been deleted", func() {
						_, err := api.Get(ctx, &pb.GetNodeSessionRequest{DevAddr: "06020304"})
						So(err, ShouldNotBeNil)
					})
				})
			})
		})
	})
}
func TestHandleJoinRequestPackets(t *testing.T) {
	conf := common.GetTestConfig()

	Convey("Given a dummy gateway and application backend and a clean Postgres and Redis database", t, func() {
		a := &testApplicationBackend{
			rxPayloadChan:           make(chan models.RXPayload, 1),
			notificationPayloadChan: make(chan interface{}, 10),
		}
		g := &testGatewayBackend{
			rxPacketChan: make(chan models.RXPacket, 1),
			txPacketChan: make(chan models.TXPacket, 1),
		}
		p := storage.NewRedisPool(conf.RedisURL)
		common.MustFlushRedis(p)
		db, err := storage.OpenDatabase(conf.PostgresDSN)
		So(err, ShouldBeNil)
		common.MustResetDB(db)

		ctx := Context{
			RedisPool:   p,
			Gateway:     g,
			Application: a,
			DB:          db,
		}

		Convey("Given a node and application in the database", func() {
			app := models.Application{
				AppEUI: [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
				Name:   "test app",
			}
			So(storage.CreateApplication(ctx.DB, app), ShouldBeNil)

			node := models.Node{
				DevEUI: [8]byte{8, 7, 6, 5, 4, 3, 2, 1},
				AppEUI: app.AppEUI,
				AppKey: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},

				RXDelay:     3,
				RX1DROffset: 2,
			}
			So(storage.CreateNode(ctx.DB, node), ShouldBeNil)

			Convey("Given a JoinRequest with correct DevEUI but incorrect AppEUI", func() {
				phy := lorawan.PHYPayload{
					MHDR: lorawan.MHDR{
						MType: lorawan.JoinRequest,
						Major: lorawan.LoRaWANR1,
					},
					MACPayload: &lorawan.JoinRequestPayload{
						AppEUI:   [8]byte{1, 2, 3, 4, 5, 6, 7, 9},
						DevEUI:   node.DevEUI,
						DevNonce: [2]byte{1, 2},
					},
				}
				So(phy.SetMIC(node.AppKey), ShouldBeNil)

				rxPacket := models.RXPacket{
					PHYPayload: phy,
					RXInfo: models.RXInfo{
						Frequency: common.Band.UplinkChannels[0].Frequency,
						DataRate:  common.Band.DataRates[common.Band.UplinkChannels[0].DataRates[0]],
					},
				}

				Convey("then handleRXPacket returns an error", func() {
					So(handleRXPacket(ctx, rxPacket), ShouldResemble, errors.New("node 0807060504030201 belongs to application 0102030405060708, 0102030405060709 was given"))
				})
			})

			Convey("Given a JoinRequest packet", func() {
				phy := lorawan.PHYPayload{
					MHDR: lorawan.MHDR{
						MType: lorawan.JoinRequest,
						Major: lorawan.LoRaWANR1,
					},
					MACPayload: &lorawan.JoinRequestPayload{
						AppEUI:   app.AppEUI,
						DevEUI:   node.DevEUI,
						DevNonce: [2]byte{1, 2},
					},
				}
				So(phy.SetMIC(node.AppKey), ShouldBeNil)

				rxPacket := models.RXPacket{
					PHYPayload: phy,
					RXInfo: models.RXInfo{
						Frequency: common.Band.UplinkChannels[0].Frequency,
						DataRate:  common.Band.DataRates[common.Band.UplinkChannels[0].DataRates[0]],
					},
				}

				Convey("When calling handleRXPacket", func() {
					So(handleRXPacket(ctx, rxPacket), ShouldBeNil)

					Convey("Then a JoinAccept was sent to the node", func() {
						txPacket := <-g.txPacketChan
						phy := txPacket.PHYPayload
						So(phy.DecryptJoinAcceptPayload(node.AppKey), ShouldBeNil)
						So(phy.MHDR.MType, ShouldEqual, lorawan.JoinAccept)

						Convey("Then it was sent after 5s", func() {
							So(txPacket.TXInfo.Timestamp, ShouldEqual, rxPacket.RXInfo.Timestamp+uint32(5*time.Second/time.Microsecond))
						})

						Convey("Then the RXDelay is set to 3s", func() {
							jaPL := phy.MACPayload.(*lorawan.JoinAcceptPayload)
							So(jaPL.RXDelay, ShouldEqual, 3)
						})

						Convey("Then the DLSettings are set correctly", func() {
							jaPL := phy.MACPayload.(*lorawan.JoinAcceptPayload)
							So(jaPL.DLSettings.RX2DataRate, ShouldEqual, uint8(common.Band.RX2DataRate))
							So(jaPL.DLSettings.RX1DROffset, ShouldEqual, node.RX1DROffset)
						})

						Convey("Then a node-session was created", func() {
							jaPL := phy.MACPayload.(*lorawan.JoinAcceptPayload)

							_, err := storage.GetNodeSession(ctx.RedisPool, jaPL.DevAddr)
							So(err, ShouldBeNil)
						})

						Convey("Then the dev-nonce was added to the used dev-nonces", func() {
							node, err := storage.GetNode(ctx.DB, node.DevEUI)
							So(err, ShouldBeNil)
							So([2]byte{1, 2}, ShouldBeIn, node.UsedDevNonces)
						})

						Convey("Then a join notification was sent to the application", func() {
							notification := <-a.notificationPayloadChan
							join, ok := notification.(models.JoinNotification)
							So(ok, ShouldBeTrue)
							So(join.DevEUI, ShouldResemble, node.DevEUI)
						})
					})
				})
			})
		})
	})
}
func TestHandleDataUpScenarios(t *testing.T) {
	conf := common.GetTestConfig()

	Convey("Given a clean state, test backends and a node-session", t, func() {
		app := &testApplicationBackend{
			rxPayloadChan:           make(chan models.RXPayload, 1),
			txPayloadChan:           make(chan models.TXPayload, 1),
			notificationPayloadChan: make(chan interface{}, 10),
		}
		gw := &testGatewayBackend{
			rxPacketChan: make(chan models.RXPacket, 1),
			txPacketChan: make(chan models.TXPacket, 1),
		}
		ctrl := &testControllerBackend{
			rxMACPayloadChan:  make(chan models.MACPayload, 2),
			txMACPayloadChan:  make(chan models.MACPayload, 1),
			rxInfoPayloadChan: make(chan models.RXInfoPayload, 1),
			errorPayloadChan:  make(chan models.ErrorPayload, 1),
		}
		p := storage.NewRedisPool(conf.RedisURL)
		common.MustFlushRedis(p)

		ctx := Context{
			RedisPool:   p,
			Gateway:     gw,
			Application: app,
			Controller:  ctrl,
		}

		ns := models.NodeSession{
			DevAddr:  [4]byte{1, 2, 3, 4},
			DevEUI:   [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
			AppSKey:  [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
			NwkSKey:  [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
			FCntUp:   8,
			FCntDown: 5,
			AppEUI:   [8]byte{8, 7, 6, 5, 4, 3, 2, 1},
		}
		So(storage.CreateNodeSession(ctx.RedisPool, ns), ShouldBeNil)

		rxInfo := models.RXInfo{
			Frequency: common.Band.UplinkChannels[0].Frequency,
			DataRate:  common.Band.DataRates[common.Band.UplinkChannels[0].DataRates[0]],
		}

		var fPortZero uint8
		var fPortOne uint8 = 1

		Convey("Given a set of test-scenarios for basic flows (nothing in queue)", func() {
			tests := []dataUpTestCase{
				// errors
				{
					Name:                    "the application backend returns an error",
					RXInfo:                  rxInfo,
					EncryptFRMPayloadKey:    ns.AppSKey,
					SetMICKey:               ns.NwkSKey,
					ApplicationBackendError: errors.New("BOOM!"),

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedFCntUp:              8,
					ExpectedFCntDown:            5,
					ExpectedHandleRXPacketError: errors.New("send rx payload to application error: BOOM!"),
				},
				{
					Name:                 "the frame-counter is invalid",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.AppSKey,
					SetMICKey:            ns.NwkSKey,

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    7,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedFCntUp:              8,
					ExpectedFCntDown:            5,
					ExpectedHandleRXPacketError: errors.New("invalid FCnt or too many dropped frames"),
				},
				{
					Name:                 "the mic is invalid",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.AppSKey,
					SetMICKey:            ns.AppSKey,

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedFCntUp:              8,
					ExpectedFCntDown:            5,
					ExpectedHandleRXPacketError: errors.New("invalid MIC"),
				},
				// basic flows
				{
					Name:                 "unconfirmed uplink data with payload",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.AppSKey,
					SetMICKey:            ns.NwkSKey,

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort:      &fPortOne,
							FRMPayload: []lorawan.Payload{&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}}},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1, Data: []byte{1, 2, 3, 4}},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 5,
				},
				{
					Name:                 "unconfirmed uplink data without payload (just a FPort)",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.AppSKey,
					SetMICKey:            ns.NwkSKey,

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 5,
				},
				{
					Name:                 "confirmed uplink data with payload",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.AppSKey,
					SetMICKey:            ns.NwkSKey,

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.ConfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort:      &fPortOne,
							FRMPayload: []lorawan.Payload{&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}}},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1, Data: []byte{1, 2, 3, 4}},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
										FCtrl: lorawan.FCtrl{
											ACK: true,
										},
									},
								},
							},
						},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6,
				},
				{
					Name:                 "confirmed uplink data without payload",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.AppSKey,
					SetMICKey:            ns.NwkSKey,

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.ConfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
										FCtrl: lorawan.FCtrl{
											ACK: true,
										},
									},
								},
							},
						},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6,
				},
				{
					Name:                 "two uplink mac commands (FOpts)",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.NwkSKey,
					SetMICKey:            ns.NwkSKey,

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
								FOpts: []lorawan.MACCommand{
									{CID: lorawan.LinkCheckReq},
									{CID: lorawan.LinkADRAns, Payload: &lorawan.LinkADRAnsPayload{ChannelMaskACK: true}},
								},
							},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedControllerRXMACPayloads: []models.MACPayload{
						{DevEUI: ns.DevEUI, MACCommand: []byte{2}},
						{DevEUI: ns.DevEUI, MACCommand: []byte{3, 1}},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 5,
				},
				{
					Name:                 "two uplink mac commands (FRMPayload)",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.NwkSKey,
					SetMICKey:            ns.NwkSKey,

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortZero,
							FRMPayload: []lorawan.Payload{
								&lorawan.MACCommand{CID: lorawan.LinkCheckReq},
								&lorawan.MACCommand{CID: lorawan.LinkADRAns, Payload: &lorawan.LinkADRAnsPayload{ChannelMaskACK: true}},
							},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedControllerRXMACPayloads: []models.MACPayload{
						{DevEUI: ns.DevEUI, FRMPayload: true, MACCommand: []byte{2}},
						{DevEUI: ns.DevEUI, FRMPayload: true, MACCommand: []byte{3, 1}},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 5,
				},
			}

			runDataUpTests(ctx, ns.DevEUI, ns.DevAddr, tests)
		})

		Convey("Given a set of test-scenarios for mac-command queue", func() {
			var fPortThree uint8 = 3

			tests := []dataUpTestCase{
				{
					Name:                 "unconfirmed uplink data + two downlink mac commands in queue (FOpts)",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.AppSKey,
					SetMICKey:            ns.NwkSKey,
					TXMACPayloadQueue: []models.MACPayload{
						{DevEUI: ns.DevEUI, MACCommand: []byte{6}},
						{DevEUI: ns.DevEUI, MACCommand: []byte{8, 3}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort:      &fPortOne,
							FRMPayload: []lorawan.Payload{&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}}},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1, Data: []byte{1, 2, 3, 4}},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
										FOpts: []lorawan.MACCommand{
											{CID: lorawan.CID(6)},
											{CID: lorawan.CID(8), Payload: &lorawan.RXTimingSetupReqPayload{Delay: 3}},
										},
									},
								},
							},
						},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6,
				},
				{
					Name:                         "unconfirmed uplink data + two downlink mac commands in queue (FOpts) + unconfirmed tx-payload in queue",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.AppSKey,
					SetMICKey:                    ns.NwkSKey,
					TXPayloadQueue: []models.TXPayload{
						{DevEUI: ns.DevEUI, FPort: 3, Data: []byte{4, 5, 6}},
					},
					TXMACPayloadQueue: []models.MACPayload{
						{DevEUI: ns.DevEUI, MACCommand: []byte{6}},
						{DevEUI: ns.DevEUI, MACCommand: []byte{8, 3}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort:      &fPortOne,
							FRMPayload: []lorawan.Payload{&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}}},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1, Data: []byte{1, 2, 3, 4}},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
										FOpts: []lorawan.MACCommand{
											{CID: lorawan.CID(6)},
											{CID: lorawan.CID(8), Payload: &lorawan.RXTimingSetupReqPayload{Delay: 3}},
										},
									},
									FPort: &fPortThree,
									FRMPayload: []lorawan.Payload{
										&lorawan.DataPayload{Bytes: []byte{4, 5, 6}},
									},
								},
							},
						},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6,
				},
				{
					Name:                         "unconfirmed uplink data + two downlink mac commands in queue (FRMPayload)",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.NwkSKey,
					SetMICKey:                    ns.NwkSKey,
					TXMACPayloadQueue: []models.MACPayload{
						{DevEUI: ns.DevEUI, FRMPayload: true, MACCommand: []byte{6}},
						{DevEUI: ns.DevEUI, FRMPayload: true, MACCommand: []byte{8, 3}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort:      &fPortOne,
							FRMPayload: []lorawan.Payload{&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}}},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1, Data: []byte{1, 2, 3, 4}},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
									},
									FPort: &fPortZero,
									FRMPayload: []lorawan.Payload{
										&lorawan.MACCommand{CID: lorawan.CID(6)},
										&lorawan.MACCommand{CID: lorawan.CID(8), Payload: &lorawan.RXTimingSetupReqPayload{Delay: 3}},
									},
								},
							},
						},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6,
				},
				{
					Name:                         "unconfirmed uplink data + two downlink mac commands in queue (FRMPayload) + unconfirmed tx-payload in queue",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.NwkSKey,
					SetMICKey:                    ns.NwkSKey,
					TXPayloadQueue: []models.TXPayload{
						{DevEUI: ns.DevEUI, FPort: 3, Data: []byte{4, 5, 6}},
					},
					TXMACPayloadQueue: []models.MACPayload{
						{DevEUI: ns.DevEUI, FRMPayload: true, MACCommand: []byte{6}},
						{DevEUI: ns.DevEUI, FRMPayload: true, MACCommand: []byte{8, 3}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort:      &fPortOne,
							FRMPayload: []lorawan.Payload{&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}}},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1, Data: []byte{1, 2, 3, 4}},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
										FCtrl: lorawan.FCtrl{
											FPending: true,
										},
									},
									FPort: &fPortZero,
									FRMPayload: []lorawan.Payload{
										&lorawan.MACCommand{CID: lorawan.CID(6)},
										&lorawan.MACCommand{CID: lorawan.CID(8), Payload: &lorawan.RXTimingSetupReqPayload{Delay: 3}},
									},
								},
							},
						},
					},
					ExpectedGetTXPayloadFromQueue: &models.TXPayload{DevEUI: ns.DevEUI, FPort: 3, Data: []byte{4, 5, 6}},
					ExpectedFCntUp:                10,
					ExpectedFCntDown:              6,
				},
				{
					Name:                 "unconfirmed uplink data + 18 bytes of MAC commands (FOpts) of which one is invalid",
					RXInfo:               rxInfo,
					EncryptFRMPayloadKey: ns.AppSKey,
					SetMICKey:            ns.NwkSKey,
					TXMACPayloadQueue: []models.MACPayload{
						{Reference: "a", DevEUI: ns.DevEUI, MACCommand: []byte{2, 10, 5}},
						{Reference: "b", DevEUI: ns.DevEUI, MACCommand: []byte{6}},
						{Reference: "c", DevEUI: ns.DevEUI, MACCommand: []byte{4, 15}},
						{Reference: "d", DevEUI: ns.DevEUI, MACCommand: []byte{4, 15, 16}}, // invalid payload, should be discarded + error notification
						{Reference: "e", DevEUI: ns.DevEUI, MACCommand: []byte{2, 10, 5}},
						{Reference: "f", DevEUI: ns.DevEUI, MACCommand: []byte{2, 10, 5}},
						{Reference: "g", DevEUI: ns.DevEUI, MACCommand: []byte{2, 10, 6}}, // this payload should stay in the queue
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort:      &fPortOne,
							FRMPayload: []lorawan.Payload{&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}}},
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedControllerErrorPayloads: []models.ErrorPayload{
						{Reference: "d", DevEUI: ns.DevEUI, Message: "unmarshal mac command error: lorawan: 1 byte of data is expected"},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1, Data: []byte{1, 2, 3, 4}},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
										FCtrl: lorawan.FCtrl{
											FPending: true,
										},
										FOpts: []lorawan.MACCommand{
											{CID: lorawan.LinkCheckAns, Payload: &lorawan.LinkCheckAnsPayload{Margin: 10, GwCnt: 5}},
											{CID: lorawan.DevStatusReq},
											{CID: lorawan.DutyCycleReq, Payload: &lorawan.DutyCycleReqPayload{MaxDCCycle: 15}},
											{CID: lorawan.LinkCheckAns, Payload: &lorawan.LinkCheckAnsPayload{Margin: 10, GwCnt: 5}},
											{CID: lorawan.LinkCheckAns, Payload: &lorawan.LinkCheckAnsPayload{Margin: 10, GwCnt: 5}},
										},
									},
								},
							},
						},
					},
					ExpectedTXMACPayloadQueue: []models.MACPayload{
						{Reference: "g", DevEUI: ns.DevEUI, MACCommand: []byte{2, 10, 6}},
					},
					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6,
				},
			}

			runDataUpTests(ctx, ns.DevEUI, ns.DevAddr, tests)
		})

		Convey("Given a set of test-scenarios for tx-payload queue", func() {
			var fPortTen uint8 = 10

			tests := []dataUpTestCase{
				{
					Name:                         "unconfirmed uplink data + one unconfirmed downlink payload in queue",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.AppSKey,
					SetMICKey:                    ns.NwkSKey,
					TXPayloadQueue: []models.TXPayload{
						{Reference: "a", DevEUI: ns.DevEUI, FPort: 10, Data: []byte{1, 2, 3, 4}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
									},
									FPort: &fPortTen,
									FRMPayload: []lorawan.Payload{
										&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}},
									},
								},
							},
						},
					},

					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6,
				},
				{
					Name:                         "unconfirmed uplink data + two unconfirmed downlink payloads in queue",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.AppSKey,
					SetMICKey:                    ns.NwkSKey,
					TXPayloadQueue: []models.TXPayload{
						{Reference: "a", DevEUI: ns.DevEUI, FPort: 10, Data: []byte{1, 2, 3, 4}},
						{Reference: "b", DevEUI: ns.DevEUI, FPort: 10, Data: []byte{4, 3, 2, 1}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
										FCtrl: lorawan.FCtrl{
											FPending: true,
										},
									},
									FPort: &fPortTen,
									FRMPayload: []lorawan.Payload{
										&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}},
									},
								},
							},
						},
					},

					ExpectedFCntUp:                10,
					ExpectedFCntDown:              6,
					ExpectedGetTXPayloadFromQueue: &models.TXPayload{Reference: "b", DevEUI: ns.DevEUI, FPort: 10, Data: []byte{4, 3, 2, 1}},
				},
				{
					Name:                         "unconfirmed uplink data + one confirmed downlink payload in queue",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.AppSKey,
					SetMICKey:                    ns.NwkSKey,
					TXPayloadQueue: []models.TXPayload{
						{Reference: "a", Confirmed: true, DevEUI: ns.DevEUI, FPort: 10, Data: []byte{1, 2, 3, 4}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.ConfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
									},
									FPort: &fPortTen,
									FRMPayload: []lorawan.Payload{
										&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}},
									},
								},
							},
						},
					},

					ExpectedFCntUp:                10,
					ExpectedFCntDown:              5, // will be incremented after the node ACKs the frame
					ExpectedGetTXPayloadFromQueue: &models.TXPayload{Reference: "a", Confirmed: true, DevEUI: ns.DevEUI, FPort: 10, Data: []byte{1, 2, 3, 4}},
				},
				{
					Name:                         "unconfirmed uplink data with ACK + one confirmed downlink payload in in-process queue",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.AppSKey,
					SetMICKey:                    ns.NwkSKey,
					TXMACPayloadInProcess:        &models.TXPayload{Reference: "a", Confirmed: true, DevEUI: ns.DevEUI, FPort: 10, Data: []byte{1, 2, 3, 4}},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
								FCtrl: lorawan.FCtrl{
									ACK: true,
								},
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1},
					},
					ExpectedApplicationNotifications: []interface{}{
						models.ACKNotification{Reference: "a", DevEUI: ns.DevEUI},
					},

					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6, // has been updated because of the ACK
				},
				{
					Name:                         "unconfirmed uplink data + two unconfirmed downlink payload in queue of which the first exceeds the max payload size (for dr 0)",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.AppSKey,
					SetMICKey:                    ns.NwkSKey,
					TXPayloadQueue: []models.TXPayload{
						{Reference: "a", DevEUI: ns.DevEUI, FPort: 10, Data: make([]byte, 52)},
						{Reference: "b", DevEUI: ns.DevEUI, FPort: 10, Data: []byte{1, 2, 3, 4}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1},
					},
					ExpectedApplicationNotifications: []interface{}{
						models.ErrorPayload{Reference: "a", DevEUI: ns.DevEUI, Message: "downlink payload max size exceeded (dr: 0, allowed: 51, got: 52)"},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
									},
									FPort: &fPortTen,
									FRMPayload: []lorawan.Payload{
										&lorawan.DataPayload{Bytes: []byte{1, 2, 3, 4}},
									},
								},
							},
						},
					},

					ExpectedFCntUp:   10,
					ExpectedFCntDown: 6,
				},
				{
					Name:                         "unconfirmed uplink data + one unconfirmed downlink payload in queue (exactly max size for dr 0) + one mac command",
					RXInfo:                       rxInfo,
					EncryptFRMPayloadKey:         ns.AppSKey,
					DecryptExpectedFRMPayloadKey: ns.AppSKey,
					SetMICKey:                    ns.NwkSKey,
					TXPayloadQueue: []models.TXPayload{
						{Reference: "a", DevEUI: ns.DevEUI, FPort: 10, Data: make([]byte, 51)},
					},
					TXMACPayloadQueue: []models.MACPayload{
						{DevEUI: ns.DevEUI, MACCommand: []byte{6}},
					},

					PHYPayload: lorawan.PHYPayload{
						MHDR: lorawan.MHDR{
							MType: lorawan.UnconfirmedDataUp,
							Major: lorawan.LoRaWANR1,
						},
						MACPayload: &lorawan.MACPayload{
							FHDR: lorawan.FHDR{
								DevAddr: ns.DevAddr,
								FCnt:    10,
							},
							FPort: &fPortOne,
						},
					},

					ExpectedControllerRXInfoPayloads: []models.RXInfoPayload{
						{DevEUI: ns.DevEUI, FCnt: 10, RXInfo: []models.RXInfo{rxInfo}},
					},
					ExpectedApplicationRXPayloads: []models.RXPayload{
						{DevEUI: ns.DevEUI, FPort: 1, GatewayCount: 1},
					},
					ExpectedGatewayTXPackets: []models.TXPacket{
						{
							TXInfo: models.TXInfo{
								Timestamp: rxInfo.Timestamp + 1000000,
								Frequency: rxInfo.Frequency,
								Power:     14,
								DataRate:  rxInfo.DataRate,
							},
							PHYPayload: lorawan.PHYPayload{
								MHDR: lorawan.MHDR{
									MType: lorawan.UnconfirmedDataDown,
									Major: lorawan.LoRaWANR1,
								},
								MACPayload: &lorawan.MACPayload{
									FHDR: lorawan.FHDR{
										DevAddr: ns.DevAddr,
										FCnt:    5,
										FCtrl: lorawan.FCtrl{
											FPending: true,
										},
										FOpts: []lorawan.MACCommand{
											{CID: lorawan.CID(6)},
										},
									},
								},
							},
						},
					},

					ExpectedFCntUp:                10,
					ExpectedFCntDown:              6,
					ExpectedGetTXPayloadFromQueue: &models.TXPayload{Reference: "a", DevEUI: ns.DevEUI, FPort: 10, Data: make([]byte, 51)},
				},
			}

			runDataUpTests(ctx, ns.DevEUI, ns.DevAddr, tests)
		})

	})

}
Beispiel #4
0
func TestNodeAPI(t *testing.T) {
	conf := common.GetTestConfig()

	Convey("Given a clean database with an application and api instance", t, func() {
		db, err := storage.OpenDatabase(conf.PostgresDSN)
		So(err, ShouldBeNil)
		common.MustResetDB(db)
		p := storage.NewRedisPool(conf.RedisURL)
		common.MustFlushRedis(p)

		ctx := context.Background()
		lsCtx := loraserver.Context{DB: db, RedisPool: p}
		api := NewNodeAPI(lsCtx)

		app := models.Application{
			AppEUI: [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
			Name:   "test app",
		}
		So(storage.CreateApplication(db, app), ShouldBeNil)

		Convey("When creating a node", func() {
			_, err := api.Create(ctx, &pb.CreateNodeRequest{
				DevEUI:      "0807060504030201",
				AppEUI:      "0102030405060708",
				AppKey:      "01020304050607080102030405060708",
				RxDelay:     1,
				Rx1DROffset: 3,
			})
			So(err, ShouldBeNil)

			Convey("The node has been created", func() {
				node, err := api.Get(ctx, &pb.GetNodeRequest{DevEUI: "0807060504030201"})
				So(err, ShouldBeNil)
				So(node, ShouldResemble, &pb.GetNodeResponse{
					DevEUI:      "0807060504030201",
					AppEUI:      "0102030405060708",
					AppKey:      "01020304050607080102030405060708",
					RxDelay:     1,
					Rx1DROffset: 3,
				})
			})

			Convey("Then listing the nodes returns a single items", func() {
				nodes, err := api.List(ctx, &pb.ListNodeRequest{
					Limit: 10,
				})
				So(err, ShouldBeNil)
				So(nodes.Result, ShouldHaveLength, 1)
				So(nodes.TotalCount, ShouldEqual, 1)
				So(nodes.Result[0], ShouldResemble, &pb.GetNodeResponse{
					DevEUI:      "0807060504030201",
					AppEUI:      "0102030405060708",
					AppKey:      "01020304050607080102030405060708",
					RxDelay:     1,
					Rx1DROffset: 3,
				})
			})

			Convey("Then listing the nodes for a given AppEUI returns a single item", func() {
				nodes, err := api.ListByAppEUI(ctx, &pb.ListNodeByAppEUIRequest{
					Limit:  10,
					AppEUI: "0102030405060708",
				})
				So(err, ShouldBeNil)
				So(nodes.Result, ShouldHaveLength, 1)
				So(nodes.TotalCount, ShouldEqual, 1)
				So(nodes.Result[0], ShouldResemble, &pb.GetNodeResponse{
					DevEUI:      "0807060504030201",
					AppEUI:      "0102030405060708",
					AppKey:      "01020304050607080102030405060708",
					RxDelay:     1,
					Rx1DROffset: 3,
				})
			})

			Convey("When updating the node", func() {
				_, err := api.Update(ctx, &pb.UpdateNodeRequest{
					DevEUI:      "0807060504030201",
					AppEUI:      "0102030405060708",
					AppKey:      "08070605040302010807060504030201",
					RxDelay:     3,
					Rx1DROffset: 1,
				})
				So(err, ShouldBeNil)

				Convey("Then the node has been updated", func() {
					node, err := api.Get(ctx, &pb.GetNodeRequest{DevEUI: "0807060504030201"})
					So(err, ShouldBeNil)
					So(node, ShouldResemble, &pb.GetNodeResponse{
						DevEUI:      "0807060504030201",
						AppEUI:      "0102030405060708",
						AppKey:      "08070605040302010807060504030201",
						RxDelay:     3,
						Rx1DROffset: 1,
					})
				})
			})

			Convey("After deleting the node", func() {
				_, err := api.Delete(ctx, &pb.DeleteNodeRequest{DevEUI: "0807060504030201"})
				So(err, ShouldBeNil)

				Convey("Then listing the nodes returns zero nodes", func() {
					nodes, err := api.List(ctx, &pb.ListNodeRequest{Limit: 10})
					So(err, ShouldBeNil)
					So(nodes.TotalCount, ShouldEqual, 0)
					So(nodes.Result, ShouldHaveLength, 0)
				})
			})

			Convey("Given a tx payload in the the queue", func() {
				So(storage.AddTXPayloadToQueue(p, models.TXPayload{
					DevEUI: [8]byte{8, 7, 6, 5, 4, 3, 2, 1},
					Data:   []byte("hello!"),
				}), ShouldBeNil)
				count, err := storage.GetTXPayloadQueueSize(p, [8]byte{8, 7, 6, 5, 4, 3, 2, 1})
				So(err, ShouldBeNil)
				So(count, ShouldEqual, 1)

				Convey("When flushing the tx-payload queue", func() {
					_, err := api.FlushTXPayloadQueue(ctx, &pb.FlushTXPayloadQueueRequest{DevEUI: "0807060504030201"})
					So(err, ShouldBeNil)

					Convey("Then the queue is empty", func() {
						count, err := storage.GetTXPayloadQueueSize(p, [8]byte{8, 7, 6, 5, 4, 3, 2, 1})
						So(err, ShouldBeNil)
						So(count, ShouldEqual, 0)
					})
				})
			})
		})
	})
}
Beispiel #5
0
func TestBackend(t *testing.T) {
	conf := getConfig()

	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 := storage.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 to the topic

			Convey("Given the MQTT client is subscribed to application/+/node/+/rx", func() {
				rxPacketChan := make(chan models.RXPayload)
				token := c.Subscribe("application/+/node/+/rx", 0, func(c mqtt.Client, msg mqtt.Message) {
					var rxPacket models.RXPayload
					if err := json.Unmarshal(msg.Payload(), &rxPacket); err != nil {
						t.Fatal(err)
					}
					rxPacketChan <- rxPacket
				})
				token.Wait()
				So(token.Error(), ShouldBeNil)

				Convey("When sending a RXPayload (from the backend)", func() {
					devEUI := lorawan.EUI64{1, 1, 1, 1, 1, 1, 1, 1}
					appEUI := lorawan.EUI64{2, 2, 2, 2, 2, 2, 2, 2}

					rxPacket := models.RXPayload{
						DevEUI: devEUI,
					}
					So(backend.SendRXPayload(appEUI, devEUI, rxPacket), ShouldBeNil)

					Convey("Then the same packet is consumed by the MQTT client", func() {
						packet := <-rxPacketChan
						So(packet, ShouldResemble, rxPacket)
					})

				})
			})

			Convey("Given the MQTT client is subscribed to application/+/node/+/join", func() {
				joinNotificationChan := make(chan models.JoinNotification)
				token := c.Subscribe("application/+/node/+/join", 0, func(c mqtt.Client, msg mqtt.Message) {
					var join models.JoinNotification
					if err := json.Unmarshal(msg.Payload(), &join); err != nil {
						t.Fatal(err)
					}
					joinNotificationChan <- join
				})
				token.Wait()
				So(token.Error(), ShouldBeNil)

				Convey("When sending a join notification (from the backend)", func() {
					devEUI := lorawan.EUI64{1, 1, 1, 1, 1, 1, 1, 1}
					appEUI := lorawan.EUI64{2, 2, 2, 2, 2, 2, 2, 2}

					join := models.JoinNotification{
						DevEUI: devEUI,
					}
					So(backend.SendNotification(appEUI, devEUI, models.JoinNotificationType, join), ShouldBeNil)

					Convey("Then the same packet is consumed by the MQTT client", func() {
						packet := <-joinNotificationChan
						So(packet, ShouldResemble, join)
					})
				})
			})

			Convey("Given a TXPayload is published by the MQTT client", func() {
				pl := models.TXPayload{
					Confirmed: false,
					DevEUI:    [8]byte{8, 7, 6, 5, 4, 3, 2, 1},
					FPort:     1,
					Data:      []byte("hello!"),
				}
				b, err := json.Marshal(pl)
				So(err, ShouldBeNil)
				token := c.Publish("application/0102030405060708/node/0807060504030201/tx", 0, false, b)
				token.Wait()
				So(token.Error(), ShouldBeNil)

				Convey("Then the same packet is received by the backend", func() {
					p := <-backend.TXPayloadChan()
					So(p, ShouldResemble, pl)

					Convey("When the topic DevEUI does not match the payload DevEUI", func() {
						token := c.Publish("application/0102030405060708/node/0707060504030201/tx", 0, false, b)
						token.Wait()
						So(token.Error(), ShouldBeNil)

						Convey("Then the packet is discarded", func() {
							var received bool
							select {
							case <-backend.TXPayloadChan():
								received = true
							case <-time.After(time.Millisecond * 100):
								// nothing to do
							}
							So(received, ShouldBeFalse)
						})
					})
				})
			})
		})
	})
}
Beispiel #6
0
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 := storage.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)
						})
					})
				})
			})
		})
	})
}
Beispiel #7
0
func run(c *cli.Context) error {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// 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)
	}
	common.Band = bandConfig

	log.WithFields(log.Fields{
		"version": version,
		"net_id":  netID.String(),
		"band":    c.String("band"),
		"docs":    "https://docs.loraserver.io/",
	}).Info("starting LoRa Server")

	// connect to the database
	log.Info("connecting to postgresql")
	db, err := storage.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 := storage.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)
	}

	// setup controller backend
	ctrl, err := controller.NewBackend(rp, c.String("controller-mqtt-server"), c.String("controller-mqtt-username"), c.String("controller-mqtt-password"))
	if err != nil {
		log.Fatalf("controller-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")
	}

	lsCtx := loraserver.Context{
		DB:          db,
		RedisPool:   rp,
		Gateway:     gw,
		Application: app,
		Controller:  ctrl,
		NetID:       netID,
	}

	// start the loraserver
	server := loraserver.NewServer(lsCtx)
	if err := server.Start(); err != nil {
		log.Fatal(err)
	}

	// setup the grpc api
	go func() {
		server := api.GetGRPCServer(ctx, lsCtx)
		list, err := net.Listen("tcp", c.String("grpc-bind"))
		if err != nil {
			log.Fatalf("error creating gRPC listener: %s", err)
		}
		log.WithField("bind", c.String("grpc-bind")).Info("starting gRPC server")
		log.Fatal(server.Serve(list))
	}()

	// setup the http server
	r := mux.NewRouter()

	// setup json api
	jsonHandler, err := api.GetJSONGateway(ctx, lsCtx, c.String("grpc-bind"))
	if err != nil {
		log.Fatalf("get json gateway error: %s", err)
	}
	log.WithField("path", "/api/v1").Info("registering api handler and documentation endpoint")
	r.HandleFunc("/api/v1", api.SwaggerHandlerFunc).Methods("get")
	r.PathPrefix("/api/v1/").Handler(jsonHandler)

	// setup static file server (for the gui)
	log.WithField("path", "/").Info("registering gui handler")
	r.PathPrefix("/").Handler(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 rest api / gui server")
		log.Fatal(http.ListenAndServe(c.String("http-bind"), r))
	}()

	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")
	}

	return nil
}
Beispiel #8
0
func TestCollectAndCallOnce(t *testing.T) {
	conf := common.GetTestConfig()

	Convey("Given a Redis connection pool", t, func() {
		p := storage.NewRedisPool(conf.RedisURL)
		c := p.Get()
		_, err := c.Do("FLUSHALL")
		So(err, ShouldBeNil)
		c.Close()

		Convey("Given a single LoRaWAN packet", func() {
			phy := lorawan.PHYPayload{
				MHDR: lorawan.MHDR{
					MType: lorawan.UnconfirmedDataUp,
					Major: lorawan.LoRaWANR1,
				},
				MIC:        [4]byte{1, 2, 3, 4},
				MACPayload: &lorawan.MACPayload{},
			}

			testTable := []struct {
				Gateways []lorawan.EUI64
				Count    int
			}{
				{
					[]lorawan.EUI64{
						{1, 1, 1, 1, 1, 1, 1, 1},
					},
					1,
				}, {
					[]lorawan.EUI64{
						{1, 1, 1, 1, 1, 1, 1, 1},
						{2, 2, 2, 2, 2, 2, 2, 2},
					},
					2,
				}, {
					[]lorawan.EUI64{
						{1, 1, 1, 1, 1, 1, 1, 1},
						{2, 2, 2, 2, 2, 2, 2, 2},
						{2, 2, 2, 2, 2, 2, 2, 2},
					},
					2,
				},
			}

			for i, test := range testTable {
				Convey(fmt.Sprintf("When running test %d, then %d packets are expected", i, test.Count), func() {
					var received int
					var called int

					cb := func(packets RXPackets) error {
						called = called + 1
						received = len(packets)
						return nil
					}

					var wg sync.WaitGroup
					for _, gw := range test.Gateways {
						wg.Add(1)
						packet := models.RXPacket{
							RXInfo: models.RXInfo{
								MAC: gw,
							},
							PHYPayload: phy,
						}
						go func() {
							err := collectAndCallOnce(p, packet, cb)
							if err != nil {
								t.Error(err)
							}
							wg.Done()
						}()
					}
					wg.Wait()

					So(called, ShouldEqual, 1)
					So(received, ShouldEqual, test.Count)
				})
			}
		})

	})
}