diff --git a/pkg/networkserver/grpc_gsns_internal_test.go b/pkg/networkserver/grpc_gsns_internal_test.go index 71e518224f..3685563677 100644 --- a/pkg/networkserver/grpc_gsns_internal_test.go +++ b/pkg/networkserver/grpc_gsns_internal_test.go @@ -15,10 +15,16 @@ package networkserver import ( + "context" "testing" + "time" + pbtypes "github.com/gogo/protobuf/types" "github.com/smartystreets/assertions" "go.thethings.network/lorawan-stack/pkg/component" + "go.thethings.network/lorawan-stack/pkg/encoding/lorawan" + "go.thethings.network/lorawan-stack/pkg/events" + "go.thethings.network/lorawan-stack/pkg/ttnpb" "go.thethings.network/lorawan-stack/pkg/types" "go.thethings.network/lorawan-stack/pkg/util/test" "go.thethings.network/lorawan-stack/pkg/util/test/assertions/should" @@ -94,3 +100,594 @@ func TestNewDevAddr(t *testing.T) { a.So(seen[ns.devAddrPrefixes[2]], should.BeGreaterThan, 0) } } + +func TestMatchAndHandleUplink(t *testing.T) { + netID := test.Must(types.NewNetID(2, []byte{1, 2, 3})).(types.NetID) + + const appIDString = "match-and-handle-uplink-test-app-id" + appID := ttnpb.ApplicationIdentifiers{ApplicationID: appIDString} + const devID = "match-and-handle-uplink-test-dev-id" + + devAddr := types.DevAddr{0x42, 0x00, 0x00, 0x00} + + fNwkSIntKey := types.AES128Key{0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + nwkSEncKey := types.AES128Key{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + sNwkSIntKey := types.AES128Key{0x42, 0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + + correlationIDs := [...]string{ + "match-and-handle-uplink-test-1", + "match-and-handle-uplink-test-2", + } + + start := time.Now().UTC() + + makeABPIdentifiers := func(devAddr types.DevAddr) *ttnpb.EndDeviceIdentifiers { + return &ttnpb.EndDeviceIdentifiers{ + ApplicationIdentifiers: appID, + DeviceID: devID, + + DevAddr: &devAddr, + } + } + + makeSessionKeys := func(ver ttnpb.MACVersion) *ttnpb.SessionKeys { + sk := &ttnpb.SessionKeys{ + FNwkSIntKey: &ttnpb.KeyEnvelope{ + Key: &fNwkSIntKey, + }, + NwkSEncKey: &ttnpb.KeyEnvelope{ + Key: &nwkSEncKey, + }, + SNwkSIntKey: &ttnpb.KeyEnvelope{ + Key: &sNwkSIntKey, + }, + SessionKeyID: []byte("match-and-handle-uplink-test-session-key-id"), + } + if ver.Compare(ttnpb.MAC_V1_1) < 0 { + sk.NwkSEncKey = sk.FNwkSIntKey + sk.SNwkSIntKey = sk.FNwkSIntKey + } + return CopySessionKeys(sk) + } + + makeSession := func(ver ttnpb.MACVersion, devAddr types.DevAddr, lastFCntUp uint32) *ttnpb.Session { + return &ttnpb.Session{ + DevAddr: devAddr, + LastFCntUp: lastFCntUp, + SessionKeys: *makeSessionKeys(ver), + } + } + + makeUplink := func(pld *ttnpb.MACPayload, confirmed bool, fCnt, confFCnt uint32, txDRIdx ttnpb.DataRateIndex, txChIdx uint8, gtwChIdx uint32, sets ttnpb.TxSettings) *ttnpb.UplinkMessage { + mType := ttnpb.MType_UNCONFIRMED_UP + if confirmed { + mType = ttnpb.MType_CONFIRMED_UP + } + msg := ttnpb.Message{ + MHDR: ttnpb.MHDR{ + MType: mType, + Major: ttnpb.Major_LORAWAN_R1, + }, + Payload: &ttnpb.Message_MACPayload{ + MACPayload: pld, + }, + } + + rawPayload := MustAppendUplinkMIC(sNwkSIntKey, fNwkSIntKey, confFCnt, uint8(txDRIdx), txChIdx, pld.DevAddr, fCnt, test.Must(lorawan.MarshalMessage(msg)).([]byte)...) + msg.MIC = rawPayload[len(rawPayload)-4:] + return &ttnpb.UplinkMessage{ + CorrelationIDs: correlationIDs[:], + GatewayChannelIndex: gtwChIdx, + Payload: &msg, + RawPayload: rawPayload, + ReceivedAt: start, + RxMetadata: MakeRxMetadataSlice(), + Settings: sets, + } + } + + makeLinkCheckEvents := func(pld *ttnpb.MACCommand_LinkCheckAns) []events.DefinitionDataClosure { + return []events.DefinitionDataClosure{ + evtReceiveLinkCheckRequest.BindData(nil), + evtEnqueueLinkCheckAnswer.BindData(pld), + } + } + + applicationDownlinkCorrelationIDs := [...]string{ + "application-downlink-correlation-id-1", + "application-downlink-correlation-id-2", + } + + makeApplicationDownlink := func() *ttnpb.ApplicationDownlink { + return &ttnpb.ApplicationDownlink{ + SessionKeyID: []byte("application-downlink-key"), + FPort: 0x01, + FCnt: 0x44, + FRMPayload: []byte("application-downlink-frm-payload"), + CorrelationIDs: applicationDownlinkCorrelationIDs[:], + } + } + + makeDownlinkNack := func() *ttnpb.ApplicationUp { + return &ttnpb.ApplicationUp{ + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + CorrelationIDs: append(applicationDownlinkCorrelationIDs[:], correlationIDs[:]...), + Up: &ttnpb.ApplicationUp_DownlinkNack{ + DownlinkNack: makeApplicationDownlink(), + }, + } + } + + makeDownlinkAck := func() *ttnpb.ApplicationUp { + return &ttnpb.ApplicationUp{ + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + CorrelationIDs: append(applicationDownlinkCorrelationIDs[:], correlationIDs[:]...), + Up: &ttnpb.ApplicationUp_DownlinkAck{ + DownlinkAck: makeApplicationDownlink(), + }, + } + } + + for _, tc := range []struct { + Name string + Uplink *ttnpb.UplinkMessage + Deduplicated bool + Devices []*ttnpb.EndDevice + DeviceAssertion func(ctx context.Context, dev *matchedDevice, up *ttnpb.UplinkMessage) bool + ErrorAssertion func(ctx context.Context, err error) bool + }{ + { + Name: "1.1/Does not support 32-bit FCnt/FCnt reset/No pending application downlink", + Uplink: makeUplink( + &ttnpb.MACPayload{ + FHDR: ttnpb.FHDR{ + DevAddr: devAddr, + FCnt: 12, + FOpts: MustEncryptUplink(nwkSEncKey, devAddr, 12, 0x02), + }, + FPort: 0x01, + FRMPayload: []byte("test-frm-payload"), + }, + false, + 12, + 0, + ttnpb.DATA_RATE_2, + 1, + 1, + ttnpb.TxSettings{ + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 10, + }}, + }, + EnableCRC: true, + Frequency: 868300000, + Timestamp: 42, + }, + ), + Devices: []*ttnpb.EndDevice{ + { + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: MakeDefaultUS915MACState(ttnpb.CLASS_B, ttnpb.MAC_V1_1), + Session: makeSession(ttnpb.MAC_V1_1, devAddr, 33), + MACSettings: &ttnpb.MACSettings{ + ResetsFCnt: &pbtypes.BoolValue{Value: true}, + Supports32BitFCnt: &pbtypes.BoolValue{Value: false}, + }, + }, + }, + DeviceAssertion: func(ctx context.Context, dev *matchedDevice, up *ttnpb.UplinkMessage) bool { + a := assertions.New(test.MustTFromContext(ctx)) + if !a.So(dev, should.NotBeNil) || + !a.So(dev.Device, should.NotBeNil) || + !a.So(dev.Device.Session, should.NotBeNil) { + return false + } + session := makeSession(ttnpb.MAC_V1_1, devAddr, 12) + session.StartedAt = dev.Device.Session.StartedAt + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1) + macState.RxWindowsAvailable = true + expectedDev := &matchedDevice{ + logger: dev.logger, + ChannelIndex: 1, + DataRateIndex: ttnpb.DATA_RATE_2, + DeferredMACHandlers: dev.DeferredMACHandlers, + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: macState, + Session: session, + MACSettings: &ttnpb.MACSettings{ + ResetsFCnt: &pbtypes.BoolValue{Value: true}, + Supports32BitFCnt: &pbtypes.BoolValue{Value: false}, + }, + }, + FCnt: 12, + FCntReset: true, + NbTrans: 1, + SetPaths: []string{ + "mac_state", + "pending_mac_state", + "pending_session", + "session", + }, + } + if !a.So([]time.Time{start, dev.Device.Session.StartedAt, time.Now()}, should.BeChronological) || + !a.So(dev.DeferredMACHandlers, should.HaveLength, 1) || + !a.So(dev, should.Resemble, expectedDev) { + return false + } + + linkCheckAns := MakeLinkCheckAns(MakeRxMetadataSlice()...) + expectedEvents := map[int][]events.DefinitionDataClosure{ + 0: makeLinkCheckEvents(linkCheckAns.GetLinkCheckAns()), + } + for i, h := range dev.DeferredMACHandlers { + evs, err := h(ctx, dev.Device, up) + if !a.So(err, should.BeNil) || !a.So(evs, should.ResembleEventDefinitionDataClosures, expectedEvents[i]) { + return false + } + } + expectedDev.Device.MACState.QueuedResponses = []*ttnpb.MACCommand{ + linkCheckAns, + } + return a.So(dev, should.Resemble, expectedDev) + }, + ErrorAssertion: func(ctx context.Context, err error) bool { + return assertions.New(test.MustTFromContext(ctx)).So(err, should.BeNil) + }, + }, + + { + Name: "1.1/Does not support 32-bit FCnt/FCnt reset/Pending application downlink", + Uplink: makeUplink( + &ttnpb.MACPayload{ + FHDR: ttnpb.FHDR{ + DevAddr: devAddr, + FCnt: 12, + FOpts: MustEncryptUplink(nwkSEncKey, devAddr, 12, 0x02), + }, + FPort: 0x01, + FRMPayload: []byte("test-frm-payload"), + }, + false, + 12, + 0, + ttnpb.DATA_RATE_2, + 1, + 1, + ttnpb.TxSettings{ + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 10, + }}, + }, + EnableCRC: true, + Frequency: 868300000, + Timestamp: 42, + }, + ), + Devices: []*ttnpb.EndDevice{ + { + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: func() *ttnpb.MACState { + macState := MakeDefaultUS915MACState(ttnpb.CLASS_B, ttnpb.MAC_V1_1) + macState.PendingApplicationDownlink = makeApplicationDownlink() + return macState + }(), + Session: makeSession(ttnpb.MAC_V1_1, devAddr, 33), + MACSettings: &ttnpb.MACSettings{ + ResetsFCnt: &pbtypes.BoolValue{Value: true}, + Supports32BitFCnt: &pbtypes.BoolValue{Value: false}, + }, + }, + }, + DeviceAssertion: func(ctx context.Context, dev *matchedDevice, up *ttnpb.UplinkMessage) bool { + a := assertions.New(test.MustTFromContext(ctx)) + if !a.So(dev, should.NotBeNil) || + !a.So(dev.Device, should.NotBeNil) || + !a.So(dev.Device.Session, should.NotBeNil) { + return false + } + session := makeSession(ttnpb.MAC_V1_1, devAddr, 12) + session.StartedAt = dev.Device.Session.StartedAt + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1) + macState.RxWindowsAvailable = true + expectedDev := &matchedDevice{ + logger: dev.logger, + ChannelIndex: 1, + DataRateIndex: ttnpb.DATA_RATE_2, + DeferredMACHandlers: dev.DeferredMACHandlers, + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: macState, + Session: session, + MACSettings: &ttnpb.MACSettings{ + ResetsFCnt: &pbtypes.BoolValue{Value: true}, + Supports32BitFCnt: &pbtypes.BoolValue{Value: false}, + }, + }, + FCnt: 12, + FCntReset: true, + NbTrans: 1, + SetPaths: []string{ + "mac_state", + "pending_mac_state", + "pending_session", + "session", + }, + QueuedApplicationUplinks: []*ttnpb.ApplicationUp{ + makeDownlinkNack(), + }, + } + if !a.So([]time.Time{start, dev.Device.Session.StartedAt, time.Now()}, should.BeChronological) || + !a.So(dev.DeferredMACHandlers, should.HaveLength, 1) || + !a.So(dev, should.Resemble, expectedDev) { + return false + } + + linkCheckAns := MakeLinkCheckAns(MakeRxMetadataSlice()...) + expectedEvents := map[int][]events.DefinitionDataClosure{ + 0: makeLinkCheckEvents(linkCheckAns.GetLinkCheckAns()), + } + for i, h := range dev.DeferredMACHandlers { + evs, err := h(ctx, dev.Device, up) + if !a.So(err, should.BeNil) || !a.So(evs, should.ResembleEventDefinitionDataClosures, expectedEvents[i]) { + return false + } + } + expectedDev.Device.MACState.QueuedResponses = []*ttnpb.MACCommand{ + linkCheckAns, + } + return a.So(dev, should.Resemble, expectedDev) + }, + ErrorAssertion: func(ctx context.Context, err error) bool { + return assertions.New(test.MustTFromContext(ctx)).So(err, should.BeNil) + }, + }, + + { + Name: "1.1/Supports 32-bit FCnt/FCnt reset/No pending application downlink", + Uplink: makeUplink( + &ttnpb.MACPayload{ + FHDR: ttnpb.FHDR{ + DevAddr: devAddr, + FCnt: 12, + FOpts: MustEncryptUplink(nwkSEncKey, devAddr, 12, 0x02), + }, + FPort: 0x01, + FRMPayload: []byte("test-frm-payload"), + }, + false, + 12, + 0, + ttnpb.DATA_RATE_2, + 1, + 1, + ttnpb.TxSettings{ + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 10, + }}, + }, + EnableCRC: true, + Frequency: 868300000, + Timestamp: 42, + }, + ), + Devices: []*ttnpb.EndDevice{ + { + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: MakeDefaultUS915MACState(ttnpb.CLASS_B, ttnpb.MAC_V1_1), + Session: makeSession(ttnpb.MAC_V1_1, devAddr, 33), + MACSettings: &ttnpb.MACSettings{ + ResetsFCnt: &pbtypes.BoolValue{Value: true}, + }, + }, + }, + DeviceAssertion: func(ctx context.Context, dev *matchedDevice, up *ttnpb.UplinkMessage) bool { + a := assertions.New(test.MustTFromContext(ctx)) + if !a.So(dev, should.NotBeNil) || + !a.So(dev.Device, should.NotBeNil) || + !a.So(dev.Device.Session, should.NotBeNil) { + return false + } + session := makeSession(ttnpb.MAC_V1_1, devAddr, 12) + session.StartedAt = dev.Device.Session.StartedAt + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1) + macState.RxWindowsAvailable = true + expectedDev := &matchedDevice{ + logger: dev.logger, + ChannelIndex: 1, + DataRateIndex: ttnpb.DATA_RATE_2, + DeferredMACHandlers: dev.DeferredMACHandlers, + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: macState, + Session: session, + MACSettings: &ttnpb.MACSettings{ + ResetsFCnt: &pbtypes.BoolValue{Value: true}, + }, + }, + FCnt: 12, + FCntReset: true, + NbTrans: 1, + SetPaths: []string{ + "mac_state", + "pending_mac_state", + "pending_session", + "session", + }, + } + if !a.So([]time.Time{start, dev.Device.Session.StartedAt, time.Now()}, should.BeChronological) || + !a.So(dev.DeferredMACHandlers, should.HaveLength, 1) || + !a.So(dev, should.Resemble, expectedDev) { + return false + } + + linkCheckAns := MakeLinkCheckAns(MakeRxMetadataSlice()...) + expectedEvents := map[int][]events.DefinitionDataClosure{ + 0: makeLinkCheckEvents(linkCheckAns.GetLinkCheckAns()), + } + for i, h := range dev.DeferredMACHandlers { + evs, err := h(ctx, dev.Device, up) + if !a.So(err, should.BeNil) || !a.So(evs, should.ResembleEventDefinitionDataClosures, expectedEvents[i]) { + return false + } + } + expectedDev.Device.MACState.QueuedResponses = []*ttnpb.MACCommand{ + linkCheckAns, + } + return a.So(dev, should.Resemble, expectedDev) + }, + ErrorAssertion: func(ctx context.Context, err error) bool { + return assertions.New(test.MustTFromContext(ctx)).So(err, should.BeNil) + }, + }, + + { + Name: "1.1/Ack", + Uplink: makeUplink( + &ttnpb.MACPayload{ + FHDR: ttnpb.FHDR{ + DevAddr: devAddr, + FCnt: 12, + FCtrl: ttnpb.FCtrl{ + Ack: true, + }, + FOpts: MustEncryptUplink(nwkSEncKey, devAddr, 12, 0x02), + }, + FPort: 0x01, + FRMPayload: []byte("test-frm-payload"), + }, + false, + 12, + 0, + ttnpb.DATA_RATE_2, + 1, + 1, + ttnpb.TxSettings{ + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 10, + }}, + }, + EnableCRC: true, + Frequency: 868300000, + Timestamp: 42, + }, + ), + Devices: []*ttnpb.EndDevice{ + { + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: func() *ttnpb.MACState { + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1) + macState.PendingApplicationDownlink = makeApplicationDownlink() + return macState + }(), + Session: makeSession(ttnpb.MAC_V1_1, devAddr, 10), + RecentDownlinks: []*ttnpb.DownlinkMessage{ + {}, + }, + }, + }, + DeviceAssertion: func(ctx context.Context, dev *matchedDevice, up *ttnpb.UplinkMessage) bool { + a := assertions.New(test.MustTFromContext(ctx)) + if !a.So(dev, should.NotBeNil) || + !a.So(dev.Device, should.NotBeNil) || + !a.So(dev.Device.Session, should.NotBeNil) { + return false + } + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1) + macState.RxWindowsAvailable = true + expectedDev := &matchedDevice{ + logger: dev.logger, + ChannelIndex: 1, + DataRateIndex: ttnpb.DATA_RATE_2, + DeferredMACHandlers: dev.DeferredMACHandlers, + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeABPIdentifiers(devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: macState, + Session: makeSession(ttnpb.MAC_V1_1, devAddr, 12), + RecentDownlinks: []*ttnpb.DownlinkMessage{ + {}, + }, + }, + FCnt: 12, + NbTrans: 1, + SetPaths: []string{ + "mac_state", + "pending_mac_state", + "pending_session", + "session", + }, + QueuedApplicationUplinks: []*ttnpb.ApplicationUp{ + makeDownlinkAck(), + }, + } + if !a.So(dev.DeferredMACHandlers, should.HaveLength, 1) || + !a.So(dev, should.Resemble, expectedDev) { + return false + } + + linkCheckAns := MakeLinkCheckAns(MakeRxMetadataSlice()...) + expectedEvents := map[int][]events.DefinitionDataClosure{ + 0: makeLinkCheckEvents(linkCheckAns.GetLinkCheckAns()), + } + for i, h := range dev.DeferredMACHandlers { + evs, err := h(ctx, dev.Device, up) + if !a.So(err, should.BeNil) || !a.So(evs, should.ResembleEventDefinitionDataClosures, expectedEvents[i]) { + return false + } + } + expectedDev.Device.MACState.QueuedResponses = []*ttnpb.MACCommand{ + linkCheckAns, + } + return a.So(dev, should.Resemble, expectedDev) + }, + ErrorAssertion: func(ctx context.Context, err error) bool { + return assertions.New(test.MustTFromContext(ctx)).So(err, should.BeNil) + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + a := assertions.New(t) + + ns, ctx, env, stop := StartTest(t, Config{NetID: netID}, (1<<10)*test.Delay) + defer stop() + + <-env.DownlinkTasks.Pop + + dev, err := ns.matchAndHandleDataUplink(ctx, CopyUplinkMessage(tc.Uplink), tc.Deduplicated, CopyEndDevices(tc.Devices...)...) + a.So(tc.DeviceAssertion(ctx, dev, CopyUplinkMessage(tc.Uplink)), should.BeTrue) + a.So(tc.ErrorAssertion(ctx, err), should.BeTrue) + }) + } +} diff --git a/pkg/networkserver/grpc_gsns_test.go b/pkg/networkserver/grpc_gsns_test.go index 0ccb4607e6..ac37566675 100644 --- a/pkg/networkserver/grpc_gsns_test.go +++ b/pkg/networkserver/grpc_gsns_test.go @@ -23,72 +23,46 @@ import ( "time" pbtypes "github.com/gogo/protobuf/types" - "github.com/mohae/deepcopy" "github.com/smartystreets/assertions" - clusterauth "go.thethings.network/lorawan-stack/pkg/auth/cluster" - "go.thethings.network/lorawan-stack/pkg/band" - "go.thethings.network/lorawan-stack/pkg/component" - "go.thethings.network/lorawan-stack/pkg/crypto" - "go.thethings.network/lorawan-stack/pkg/encoding/lorawan" "go.thethings.network/lorawan-stack/pkg/errors" - "go.thethings.network/lorawan-stack/pkg/frequencyplans" - "go.thethings.network/lorawan-stack/pkg/log" + "go.thethings.network/lorawan-stack/pkg/events" . "go.thethings.network/lorawan-stack/pkg/networkserver" - "go.thethings.network/lorawan-stack/pkg/networkserver/redis" "go.thethings.network/lorawan-stack/pkg/ttnpb" "go.thethings.network/lorawan-stack/pkg/types" - "go.thethings.network/lorawan-stack/pkg/unique" "go.thethings.network/lorawan-stack/pkg/util/test" "go.thethings.network/lorawan-stack/pkg/util/test/assertions/should" "google.golang.org/grpc" ) -var ( - DuplicateCount = 6 - DeviceCount = 100 - - Downlinks = [...]*ttnpb.ApplicationDownlink{ - {FCnt: 0}, - {FCnt: 1}, - {FCnt: 2}, - {FCnt: 3}, - {FCnt: 4}, - {FCnt: 5}, - {FCnt: 6}, - {FCnt: 7}, - {FCnt: 8}, - {FCnt: 9}, - } -) - -type UplinkHandler interface { - HandleUplink(context.Context, *ttnpb.UplinkMessage) (*pbtypes.Empty, error) -} - -func sendUplinkDuplicates(t *testing.T, h UplinkHandler, windowEndCh chan WindowEndRequest, ctx context.Context, msg *ttnpb.UplinkMessage, n int) []*ttnpb.RxMetadata { +func sendUplinkDuplicates(ctx context.Context, handle func(ctx context.Context, up *ttnpb.UplinkMessage) <-chan error, windowEndCh <-chan WindowEndRequest, makeMessage func(decoded bool) *ttnpb.UplinkMessage, start time.Time, n int) []*ttnpb.RxMetadata { + t := test.MustTFromContext(ctx) t.Helper() a := assertions.New(t) var weResp chan<- time.Time select { - case weReq := <-windowEndCh: - msg := CopyUplinkMessage(msg) - - a.So(weReq.Message.ReceivedAt, should.HappenBefore, time.Now()) - msg.ReceivedAt = weReq.Message.ReceivedAt - - a.So(weReq.Message.CorrelationIDs, should.NotBeEmpty) - msg.CorrelationIDs = weReq.Message.CorrelationIDs - - a.So(weReq.Message, should.Resemble, msg) - a.So(weReq.Context, should.HaveParentContext, ctx) - - weResp = weReq.Response - - case <-time.After(Timeout): + case <-ctx.Done(): t.Fatal("Timed out while waiting for window end request to arrive") return nil + + case req := <-windowEndCh: + expectedMsg := makeMessage(true) + for _, id := range expectedMsg.CorrelationIDs { + a.So(req.Message.CorrelationIDs, should.Contain, id) + } + a.So(len(req.Message.CorrelationIDs), should.BeGreaterThan, len(expectedMsg.CorrelationIDs)) + for _, md := range expectedMsg.RxMetadata { + a.So(req.Message.RxMetadata, should.Contain, md) + } + a.So(len(req.Message.RxMetadata), should.BeGreaterThanOrEqualTo, len(expectedMsg.RxMetadata)) + a.So([]time.Time{start, req.Message.ReceivedAt, time.Now()}, should.BeChronological) + expectedMsg.ReceivedAt = req.Message.ReceivedAt + expectedMsg.CorrelationIDs = req.Message.CorrelationIDs + expectedMsg.RxMetadata = req.Message.RxMetadata + a.So(req.Message, should.HaveEmptyDiff, expectedMsg) + + weResp = req.Response } mdCh := make(chan *ttnpb.RxMetadata, n) @@ -102,2297 +76,2741 @@ func sendUplinkDuplicates(t *testing.T, h UplinkHandler, windowEndCh chan Window go func() { defer wg.Done() - msg := CopyUplinkMessage(msg) + msg := makeMessage(false) msg.RxMetadata = nil - n := rand.Intn(10) + n := 1 + rand.Intn(10) for i := 0; i < n; i++ { md := ttnpb.NewPopulatedRxMetadata(test.Randy, false) msg.RxMetadata = append(msg.RxMetadata, md) mdCh <- md } - _, err := h.HandleUplink(ctx, msg) + err := <-handle(ctx, msg) a.So(err, should.BeNil) }() } go func() { - if !a.So(test.WaitTimeout(Timeout, wg.Wait), should.BeTrue) { - t.FailNow() + if !test.WaitContext(ctx, wg.Wait) { + t.Log("Timed out while waiting for duplicate uplinks to be processed") + return } select { - case weResp <- time.Now(): + case <-ctx.Done(): + t.Log("Timed out while waiting for metadata collection to stop") + return - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for metadata collection to stop") + case weResp <- time.Now(): } close(mdCh) }() }) { - t.Fatal("Failed to send duplicates") + t.Error("Failed to send duplicates") return nil } - mds := append([]*ttnpb.RxMetadata{}, msg.RxMetadata...) + var mds []*ttnpb.RxMetadata for md := range mdCh { mds = append(mds, md) } return mds } -func handleUplinkTest() func(t *testing.T) { - return func(t *testing.T) { - authorizedCtx := clusterauth.NewContext(test.Context(), nil) - - t.Run("No device", func(t *testing.T) { - a := assertions.New(t) - - redisClient, flush := test.NewRedis(t, "networkserver_test") - defer flush() - defer redisClient.Close() - devReg := &redis.DeviceRegistry{Redis: redisClient} - - ns := test.Must(New( - component.MustNew(test.GetLogger(t), &component.Config{}), - &Config{ - Devices: devReg, - DeduplicationWindow: 42, - CooldownWindow: 42, - DownlinkTasks: &MockDownlinkTaskQueue{ - PopFunc: DownlinkTaskPopBlockFunc, - }, - })).(*NetworkServer) - ns.FrequencyPlans = frequencyplans.NewStore(test.FrequencyPlansFetcher) - test.Must(nil, ns.Start()) - defer ns.Close() +func TestHandleUplink(t *testing.T) { + dataGetPaths := [...]string{ + "frequency_plan_id", + "last_dev_status_received_at", + "lorawan_phy_version", + "lorawan_version", + "mac_settings", + "mac_state", + "multicast", + "pending_mac_state", + "pending_session", + "recent_downlinks", + "recent_uplinks", + "session", + "supports_class_b", + "supports_class_c", + "supports_join", + } - _, err := ns.HandleUplink(authorizedCtx, ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, types.AES128Key{}, types.AES128Key{}, false)) - a.So(err, should.NotBeNil) - }) + joinGetByEUIPaths := [...]string{ + "frequency_plan_id", + "lorawan_phy_version", + "lorawan_version", + "mac_settings", + "session", + "supports_class_b", + "supports_class_c", + "supports_join", + } - t.Run("No frequency match", func(t *testing.T) { - a := assertions.New(t) - - redisClient, flush := test.NewRedis(t, "networkserver_test") - defer flush() - defer redisClient.Close() - devReg := &redis.DeviceRegistry{Redis: redisClient} - - ns := test.Must(New( - component.MustNew(test.GetLogger(t), &component.Config{}), - &Config{ - Devices: devReg, - DeduplicationWindow: 42, - CooldownWindow: 42, - DownlinkTasks: &MockDownlinkTaskQueue{ - PopFunc: DownlinkTaskPopBlockFunc, - }, - })).(*NetworkServer) - ns.FrequencyPlans = frequencyplans.NewStore(test.FrequencyPlansFetcher) - test.Must(nil, ns.Start()) - defer ns.Close() - - pb := &ttnpb.EndDevice{ - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ApplicationID: ApplicationID}, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, + joinSetByEUIGetPaths := [...]string{ + "frequency_plan_id", + "lorawan_phy_version", + "queued_application_downlinks", + "recent_uplinks", + } + + joinSetByEUISetPaths := [...]string{ + "pending_mac_state", + "queued_application_downlinks", + "recent_uplinks", + } + + const duplicateCount = 6 + const fPort = 0x42 + + netID := test.Must(types.NewNetID(2, []byte{1, 2, 3})).(types.NetID) + + const appIDString = "handle-uplink-test-app-id" + appID := ttnpb.ApplicationIdentifiers{ApplicationID: appIDString} + const devID = "handle-uplink-test-dev-id" + + joinEUI := types.EUI64{0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + devEUI := types.EUI64{0x42, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + + devAddr := types.DevAddr{0x42, 0x00, 0x00, 0x00} + + fNwkSIntKey := types.AES128Key{0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + nwkSEncKey := types.AES128Key{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + sNwkSIntKey := types.AES128Key{0x42, 0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + appSKey := types.AES128Key{0x42, 0x42, 0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + + correlationIDs := [...]string{ + "handle-uplink-test-1", + "handle-uplink-test-2", + } + + now := time.Now().UTC() + + makeOTAAIdentifiers := func(devAddr *types.DevAddr) *ttnpb.EndDeviceIdentifiers { + return &ttnpb.EndDeviceIdentifiers{ + ApplicationIdentifiers: appID, + DeviceID: devID, + + DevEUI: devEUI.Copy(&types.EUI64{}), + JoinEUI: joinEUI.Copy(&types.EUI64{}), + + DevAddr: devAddr, + } + } + + makeSessionKeys := func(ver ttnpb.MACVersion) *ttnpb.SessionKeys { + sk := &ttnpb.SessionKeys{ + FNwkSIntKey: &ttnpb.KeyEnvelope{ + Key: &fNwkSIntKey, + }, + NwkSEncKey: &ttnpb.KeyEnvelope{ + Key: &nwkSEncKey, + }, + SNwkSIntKey: &ttnpb.KeyEnvelope{ + Key: &sNwkSIntKey, + }, + SessionKeyID: []byte("handle-uplink-test-session-key-id"), + } + if ver.Compare(ttnpb.MAC_V1_1) < 0 { + sk.NwkSEncKey = sk.FNwkSIntKey + sk.SNwkSIntKey = sk.FNwkSIntKey + } + return CopySessionKeys(sk) + } + + makeSession := func(ver ttnpb.MACVersion, devAddr types.DevAddr, lastFCntUp uint32) *ttnpb.Session { + return &ttnpb.Session{ + DevAddr: devAddr, + LastFCntUp: lastFCntUp, + SessionKeys: *makeSessionKeys(ver), + StartedAt: now, + } + } + + makeJoinRequest := func(decodePayload bool) *ttnpb.UplinkMessage { + msg := &ttnpb.UplinkMessage{ + CorrelationIDs: correlationIDs[:], + GatewayChannelIndex: 2, + RawPayload: []byte{ + /* MHDR */ + 0x00, + /* Join-request */ + /** JoinEUI **/ + joinEUI[7], joinEUI[6], joinEUI[5], joinEUI[4], joinEUI[3], joinEUI[2], joinEUI[1], joinEUI[0], + /** DevEUI **/ + devEUI[7], devEUI[6], devEUI[5], devEUI[4], devEUI[3], devEUI[2], devEUI[1], devEUI[0], + /** DevNonce **/ + 0x01, 0x00, + /* MIC */ + 0x03, 0x02, 0x01, 0x00, + }, + RxMetadata: MakeRxMetadataSlice(), + Settings: ttnpb.TxSettings{ + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 11, + }}, }, - LoRaWANVersion: ttnpb.MAC_V1_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - SNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &SNwkSIntKey, - }, + EnableCRC: true, + Frequency: 868500000, + Timestamp: 42, + }, + } + if decodePayload { + msg.Payload = &ttnpb.Message{ + MHDR: ttnpb.MHDR{ + MType: ttnpb.MType_JOIN_REQUEST, + Major: ttnpb.Major_LORAWAN_R1, + }, + MIC: []byte{0x03, 0x02, 0x01, 0x00}, + Payload: &ttnpb.Message_JoinRequestPayload{ + JoinRequestPayload: &ttnpb.JoinRequestPayload{ + DevEUI: devEUI, + JoinEUI: joinEUI, + DevNonce: types.DevNonce{0x00, 0x01}, }, }, - FrequencyPlanID: test.EUFrequencyPlanID, } + } + return msg + } - ret, err := devReg.SetByID(authorizedCtx, pb.ApplicationIdentifiers, pb.DeviceID, - []string{ - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "session", + makeRejoinRequest := func(decodePayload bool) *ttnpb.UplinkMessage { + msg := &ttnpb.UplinkMessage{ + CorrelationIDs: correlationIDs[:], + GatewayChannelIndex: 2, + RawPayload: []byte{ + /* MHDR */ + 0xc0, + /* Rejoin-request */ + /** Rejoin Type **/ + 0x00, + /** JoinEUI **/ + netID[2], netID[1], netID[0], + /** DevEUI **/ + devEUI[7], devEUI[6], devEUI[5], devEUI[4], devEUI[3], devEUI[2], devEUI[1], devEUI[0], + /** RJcount0 **/ + 0x01, 0x00, + /* MIC */ + 0x03, 0x02, 0x01, 0x00, + }, + RxMetadata: MakeRxMetadataSlice(), + Settings: ttnpb.TxSettings{ + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 11, + }}, }, - func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { - if !a.So(stored, should.BeNil) { - t.Fatal("Registry is not empty") - } - return CopyEndDevice(pb), []string{ - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "session", - }, nil - }) - if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) { - t.Fatalf("Failed to create device, error: %s", err) - } - pb.CreatedAt = ret.CreatedAt - pb.UpdatedAt = ret.UpdatedAt - a.So(ret, should.Resemble, pb) - - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, false) - msg.Payload.GetMACPayload().DevAddr = DevAddr - msg.Settings.Frequency = 42 - _, err = ns.HandleUplink(authorizedCtx, msg) - if a.So(err, should.BeError) { - if !a.So(errors.IsNotFound(err), should.BeTrue) { - t.Errorf("Error: %s", err) - } - } - }) - - t.Run("No data rate match", func(t *testing.T) { - a := assertions.New(t) - - redisClient, flush := test.NewRedis(t, "networkserver_test") - defer flush() - defer redisClient.Close() - devReg := &redis.DeviceRegistry{Redis: redisClient} - - ns := test.Must(New( - component.MustNew(test.GetLogger(t), &component.Config{}), - &Config{ - Devices: devReg, - DeduplicationWindow: 42, - CooldownWindow: 42, - DownlinkTasks: &MockDownlinkTaskQueue{ - PopFunc: DownlinkTaskPopBlockFunc, - }, - })).(*NetworkServer) - ns.FrequencyPlans = frequencyplans.NewStore(test.FrequencyPlansFetcher) - test.Must(nil, ns.Start()) - defer ns.Close() - - pb := &ttnpb.EndDevice{ - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ApplicationID: ApplicationID}, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, + EnableCRC: true, + Frequency: 868500000, + Timestamp: 42, + }, + } + if decodePayload { + msg.Payload = &ttnpb.Message{ + MHDR: ttnpb.MHDR{ + MType: ttnpb.MType_REJOIN_REQUEST, + Major: ttnpb.Major_LORAWAN_R1, }, - LoRaWANVersion: ttnpb.MAC_V1_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - SNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &SNwkSIntKey, - }, + MIC: []byte{0x03, 0x02, 0x01, 0x00}, + Payload: &ttnpb.Message_RejoinRequestPayload{ + RejoinRequestPayload: &ttnpb.RejoinRequestPayload{ + DevEUI: devEUI, + NetID: netID, + RejoinCnt: 1, + RejoinType: ttnpb.RejoinType_CONTEXT, }, }, - FrequencyPlanID: test.EUFrequencyPlanID, } + } + return msg + } - ret, err := devReg.SetByID(authorizedCtx, pb.ApplicationIdentifiers, pb.DeviceID, - []string{ - "created_at", - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "multicast", - "session", - "updated_at", - }, - func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { - if !a.So(stored, should.BeNil) { - t.Fatal("Registry is not empty") - } - return CopyEndDevice(pb), []string{ - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "multicast", - "session", - }, nil - }) - if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) { - t.FailNow() - } - pb.CreatedAt = ret.CreatedAt - pb.UpdatedAt = ret.UpdatedAt - a.So(ret, should.Resemble, pb) - - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, false) - msg.Payload.GetMACPayload().DevAddr = DevAddr - msg.Settings.DataRate = ttnpb.DataRate{ - Modulation: &ttnpb.DataRate_LoRa{ - LoRa: &ttnpb.LoRaDataRate{ - Bandwidth: 42, - SpreadingFactor: 42, + makeDataUplinkFRMPayload := func(fCnt uint32) []byte { + return MustEncryptUplink(appSKey, devAddr, fCnt, 't', 'e', 's', 't') + } + + makeDataUplinkSettings := func() ttnpb.TxSettings { + return ttnpb.TxSettings{ + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 10, + }}, + }, + EnableCRC: true, + Frequency: 868300000, + Timestamp: 42, + } + } + + makeDecodedDataUplinkPayload := func(fCnt uint32, fOpts []byte, payload ...byte) *ttnpb.Message { + return &ttnpb.Message{ + MHDR: ttnpb.MHDR{ + MType: ttnpb.MType_UNCONFIRMED_UP, + Major: ttnpb.Major_LORAWAN_R1, + }, + MIC: payload[len(payload)-4:], + Payload: &ttnpb.Message_MACPayload{ + MACPayload: &ttnpb.MACPayload{ + FHDR: ttnpb.FHDR{ + DevAddr: *devAddr.Copy(&types.DevAddr{}), + FCnt: fCnt, + FOpts: fOpts, }, + FPort: fPort, + FRMPayload: makeDataUplinkFRMPayload(fCnt), }, - } - _, err = ns.HandleUplink(authorizedCtx, msg) - if a.So(err, should.BeError) { - if !a.So(errors.IsNotFound(err), should.BeTrue) { - t.Errorf("Error: %s", err) - } - } - }) + }, + } + } - for _, tc := range []struct { - Name string - - Device *ttnpb.EndDevice - NextFCntUp uint32 - UplinkMessage *ttnpb.UplinkMessage - }{ - { - "1.0/unconfirmed/no ack/FCnt does not reset", - &ttnpb.EndDevice{ - MACState: &ttnpb.MACState{ - LoRaWANVersion: ttnpb.MAC_V1_0, - CurrentParameters: ttnpb.MACParameters{ - ADRAckDelay: 1, - ADRAckLimit: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - DesiredParameters: ttnpb.MACParameters{ - ADRAckDelay: 1, - ADRAckLimit: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - }, - LoRaWANVersion: ttnpb.MAC_V1_0, - LoRaWANPHYVersion: ttnpb.PHY_V1_0, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ - ApplicationID: ApplicationID, - }, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - LastFCntUp: 0x41, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - NwkSEncKey: &ttnpb.KeyEnvelope{ - Key: &NwkSEncKey, - }, - }, - }, + makeLegacyDataUplink := func(fCnt uint8, decodePayload bool) *ttnpb.UplinkMessage { + msg := &ttnpb.UplinkMessage{ + CorrelationIDs: correlationIDs[:], + GatewayChannelIndex: 1, + RawPayload: MustAppendLegacyUplinkMIC( + fNwkSIntKey, + devAddr, + uint32(fCnt), + append([]byte{ + /* MHDR */ + 0x40, + /* MACPayload */ + /** FHDR **/ + /*** DevAddr ***/ + devAddr[3], devAddr[2], devAddr[1], devAddr[0], + /*** FCtrl ***/ + 0x01, + /*** FCnt ***/ + fCnt, 0x00, + /*** FOpts ***/ + /**** LinkCheckReq ****/ + 0x02, + /** FPort **/ + fPort, }, - 0x42, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, false) - msg.Payload.GetMACPayload().FHDR.Ack = false - msg.Payload.GetMACPayload().FHDR.ADR = false - msg.Settings.Frequency = 100045 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate + makeDataUplinkFRMPayload(uint32(fCnt))..., + )..., + ), + RxMetadata: MakeRxMetadataSlice(), + Settings: makeDataUplinkSettings(), + } + if decodePayload { + msg.Payload = makeDecodedDataUplinkPayload(uint32(fCnt), []byte{0x02}, msg.RawPayload...) + msg.ReceivedAt = now + } + return msg + } - pld := msg.Payload.GetMACPayload() - pld.DevAddr = DevAddr - pld.FCnt = 0x42 + bindMakeLegacyDataUplinkFCnt := func(fCnt uint8) func(bool) *ttnpb.UplinkMessage { + return func(decoded bool) *ttnpb.UplinkMessage { + return makeLegacyDataUplink(fCnt, decoded) + } + } - msg.Payload.MIC = nil - mic := test.Must(crypto.ComputeLegacyUplinkMIC(FNwkSIntKey, DevAddr, 0x42, test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte))).([4]byte) - msg.Payload.MIC = mic[:] + makeDataUplink := func(fCnt uint8, decodePayload bool) *ttnpb.UplinkMessage { + sets := makeDataUplinkSettings() + mds := MakeRxMetadataSlice() + fOpts := MustEncryptUplink(nwkSEncKey, devAddr, uint32(fCnt), 0x02) + msg := &ttnpb.UplinkMessage{ + CorrelationIDs: correlationIDs[:], + GatewayChannelIndex: 1, + RawPayload: MustAppendUplinkMIC( + sNwkSIntKey, + fNwkSIntKey, + 0, + 2, + 1, + devAddr, + uint32(fCnt), + append( + append( + append([]byte{ + /* MHDR */ + 0x40, + /* MACPayload */ + /** FHDR **/ + /*** DevAddr ***/ + devAddr[3], devAddr[2], devAddr[1], devAddr[0], + /*** FCtrl ***/ + 0x01, + /*** FCnt ***/ + fCnt, 0x00, + }, + /*** FOpts ***/ + fOpts..., + ), + /** FPort **/ + fPort, + ), + makeDataUplinkFRMPayload(uint32(fCnt))..., + )..., + ), + RxMetadata: mds, + Settings: sets, + } + if decodePayload { + msg.Payload = makeDecodedDataUplinkPayload(uint32(fCnt), fOpts, msg.RawPayload...) + msg.ReceivedAt = now + } + return msg + } - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + bindMakeDataUplinkFCnt := func(fCnt uint8) func(bool) *ttnpb.UplinkMessage { + return func(decoded bool) *ttnpb.UplinkMessage { + return makeDataUplink(fCnt, decoded) + } + } - return msg - }(), + makeApplicationDownlink := func() *ttnpb.ApplicationDownlink { + return &ttnpb.ApplicationDownlink{ + SessionKeyID: []byte("app-down-1-session-key-id"), + FPort: fPort, + FCnt: 0x32, + FRMPayload: []byte("app-down-1-frm-payload"), + Confirmed: true, + Priority: ttnpb.TxSchedulePriority_HIGH, + CorrelationIDs: []string{ + "app-down-1-correlation-id-1", }, - { - "1.0/unconfirmed/no ack/FCnt resets", - &ttnpb.EndDevice{ - MACSettings: &ttnpb.MACSettings{ - ResetsFCnt: &pbtypes.BoolValue{Value: true}, - }, - MACState: &ttnpb.MACState{ - LoRaWANVersion: ttnpb.MAC_V1_0, - CurrentParameters: ttnpb.MACParameters{ - ADRAckDelay: 1, - ADRAckLimit: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - DesiredParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - }, - LoRaWANVersion: ttnpb.MAC_V1_0, - LoRaWANPHYVersion: ttnpb.PHY_V1_0, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ - ApplicationID: ApplicationID, - }, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - LastFCntUp: 0x42424249, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - NwkSEncKey: &ttnpb.KeyEnvelope{ - Key: &NwkSEncKey, - }, - }, - }, - }, - 0x42, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, false) - msg.Payload.GetMACPayload().FHDR.Ack = false - msg.Payload.GetMACPayload().FHDR.ADR = false - msg.Settings.Frequency = 100045 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate + } + } - pld := msg.Payload.GetMACPayload() - pld.DevAddr = DevAddr - pld.FCnt = 0x42 + makeJoinResponse := func(ver ttnpb.MACVersion) *ttnpb.JoinResponse { + return &ttnpb.JoinResponse{ + RawPayload: bytes.Repeat([]byte{0x42}, 17), + SessionKeys: *makeSessionKeys(ver), + } + } - msg.Payload.MIC = nil - mic := test.Must(crypto.ComputeLegacyUplinkMIC(FNwkSIntKey, DevAddr, 0x42, test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte))).([4]byte) - msg.Payload.MIC = mic[:] + assertHandleUplinkResponse := func(ctx context.Context, handleUplinkErrCh <-chan error, assert func(error) bool) bool { + t := test.MustTFromContext(ctx) + t.Helper() + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for NetworkServer.HandleUplink to return") + return false - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + case err := <-handleUplinkErrCh: + return assert(err) + } + } - return msg - }(), - }, - { - "1.0/confirmed/ack/FCnt does not reset", - &ttnpb.EndDevice{ - MACState: &ttnpb.MACState{ - LoRaWANVersion: ttnpb.MAC_V1_0, - PendingApplicationDownlink: ttnpb.NewPopulatedApplicationDownlink(test.Randy, false), - CurrentParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - DesiredParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - }, - LoRaWANVersion: ttnpb.MAC_V1_0, - LoRaWANPHYVersion: ttnpb.PHY_V1_0, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ - ApplicationID: ApplicationID, - }, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - LastFCntUp: 0x41, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - NwkSEncKey: &ttnpb.KeyEnvelope{ - Key: &NwkSEncKey, - }, - }, - }, - RecentDownlinks: []*ttnpb.DownlinkMessage{ - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - }, - }, - 0x42, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, true) - msg.Payload.GetMACPayload().FHDR.Ack = true - msg.Payload.GetMACPayload().FHDR.ADR = false - msg.Settings.Frequency = 100045 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate + type AsNsLinkRecvRequest struct { + Uplink *ttnpb.ApplicationUp + Response chan<- *pbtypes.Empty + } + + var errTest = errors.New("testError") - pld := msg.Payload.GetMACPayload() - pld.DevAddr = DevAddr - pld.FCnt = 0x42 + for _, tc := range []struct { + Name string + Handler func(context.Context, TestEnvironment, <-chan AsNsLinkRecvRequest, func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool + }{ + { + Name: "Invalid payload", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) - msg.Payload.MIC = nil - mic := test.Must(crypto.ComputeLegacyUplinkMIC(FNwkSIntKey, DevAddr, 0x42, test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte))).([4]byte) - msg.Payload.MIC = mic[:] + makeMsg := func(_ bool) *ttnpb.UplinkMessage { + return &ttnpb.UplinkMessage{ + RxMetadata: MakeRxMetadataSlice(), + } + } - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + handleUplinkErrCh := handle(ctx, makeMsg(false)) - return msg - }(), + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.HaveSameErrorDefinitionAs, ErrDecodePayload) + }) }, - { - "1.0/confirmed/ack/FCnt resets", - &ttnpb.EndDevice{ - MACSettings: &ttnpb.MACSettings{ - ResetsFCnt: &pbtypes.BoolValue{Value: true}, - }, - MACState: &ttnpb.MACState{ - LoRaWANVersion: ttnpb.MAC_V1_0, - PendingApplicationDownlink: ttnpb.NewPopulatedApplicationDownlink(test.Randy, false), - CurrentParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - DesiredParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - }, - LoRaWANVersion: ttnpb.MAC_V1_0, - LoRaWANPHYVersion: ttnpb.PHY_V1_0, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ - ApplicationID: ApplicationID, - }, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - LastFCntUp: 0x42424249, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - NwkSEncKey: &ttnpb.KeyEnvelope{ - Key: &NwkSEncKey, - }, - }, - }, - RecentDownlinks: []*ttnpb.DownlinkMessage{ - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - }, - }, - 0x42, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, true) - msg.Payload.GetMACPayload().FHDR.Ack = true - msg.Payload.GetMACPayload().FHDR.ADR = false - msg.Settings.Frequency = 100045 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate + }, - pld := msg.Payload.GetMACPayload() - pld.DevAddr = DevAddr - pld.FCnt = 0x42 + { + Name: "Unknown Major", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) - msg.Payload.MIC = nil - mic := test.Must(crypto.ComputeLegacyUplinkMIC(FNwkSIntKey, DevAddr, 0x42, test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte))).([4]byte) - msg.Payload.MIC = mic[:] + makeMsg := func(_ bool) *ttnpb.UplinkMessage { + return &ttnpb.UplinkMessage{ + RawPayload: []byte{ + /* MHDR */ + 0x01, + /* Join-request */ + /** JoinEUI **/ + joinEUI[7], joinEUI[6], joinEUI[5], joinEUI[4], joinEUI[3], joinEUI[2], joinEUI[1], joinEUI[0], + /** DevEUI **/ + devEUI[7], devEUI[6], devEUI[5], devEUI[4], devEUI[3], devEUI[2], devEUI[1], devEUI[0], + /** DevNonce **/ + 0x01, 0x00, + /* MIC */ + 0x03, 0x02, 0x01, 0x00, + }, + RxMetadata: MakeRxMetadataSlice(), + } + } - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + handleUplinkErrCh := handle(ctx, makeMsg(false)) - return msg - }(), + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.HaveSameErrorDefinitionAs, ErrUnsupportedLoRaWANVersion) + }) }, - { - "1.1/unconfirmed/no ack/FCnt does not reset", - &ttnpb.EndDevice{ - MACState: &ttnpb.MACState{ - LoRaWANVersion: ttnpb.MAC_V1_1, - CurrentParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - DesiredParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - }, - LoRaWANVersion: ttnpb.MAC_V1_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ - ApplicationID: ApplicationID, - }, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - LastFCntUp: 0x41, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - SNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &SNwkSIntKey, - }, - NwkSEncKey: &ttnpb.KeyEnvelope{ - Key: &NwkSEncKey, - }, - }, - }, - }, - 0x42, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, false) - msg.Payload.GetMACPayload().FHDR.Ack = false - msg.Payload.GetMACPayload().FHDR.ADR = false - msg.Settings.Frequency = 100045 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate - - pld := msg.Payload.GetMACPayload() - pld.DevAddr = DevAddr - pld.FCnt = 0x42 - - msg.Payload.MIC = nil - mic := test.Must(crypto.ComputeUplinkMIC( - SNwkSIntKey, - FNwkSIntKey, - 0, - 3, - 2, - DevAddr, - 0x42, - test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte)), - ).([4]byte) - msg.Payload.MIC = mic[:] - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + }, - return msg - }(), + { + Name: "Invalid MType", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + makeMsg := func(_ bool) *ttnpb.UplinkMessage { + return &ttnpb.UplinkMessage{ + RawPayload: bytes.Repeat([]byte{0x20}, 33), + RxMetadata: MakeRxMetadataSlice(), + } + } + + handleUplinkErrCh := handle(ctx, makeMsg(false)) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeNil) + }) }, - { - "1.1/confirmed/ack/FCnt does not reset", - &ttnpb.EndDevice{ - MACState: &ttnpb.MACState{ - LoRaWANVersion: ttnpb.MAC_V1_1, - PendingApplicationDownlink: ttnpb.NewPopulatedApplicationDownlink(test.Randy, false), - CurrentParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - DesiredParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, - }, - LoRaWANVersion: ttnpb.MAC_V1_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ - ApplicationID: ApplicationID, - }, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - LastFCntUp: 0x41, - LastConfFCntDown: 0x24, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - SNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &SNwkSIntKey, - }, - NwkSEncKey: &ttnpb.KeyEnvelope{ - Key: &NwkSEncKey, - }, + }, + + { + Name: "Proprietary MType", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + makeMsg := func(_ bool) *ttnpb.UplinkMessage { + return &ttnpb.UplinkMessage{ + RawPayload: []byte{ + /* MHDR */ + 0xe0, }, - }, - RecentDownlinks: []*ttnpb.DownlinkMessage{ - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - func() *ttnpb.DownlinkMessage { - msg := ttnpb.NewPopulatedDownlinkMessage(test.Randy, false) - msg.Payload.MHDR.MType = ttnpb.MType_CONFIRMED_DOWN - msg.Payload.GetMACPayload().FCnt = 0x24 - return msg - }(), - }, - }, - 0x42, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, true) - msg.Payload.GetMACPayload().FHDR.Ack = true - msg.Payload.GetMACPayload().FHDR.ADR = false - msg.Settings.Frequency = 100045 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate - - pld := msg.Payload.GetMACPayload() - pld.DevAddr = DevAddr - pld.FCnt = 0x42 - - msg.Payload.MIC = nil - mic := test.Must(crypto.ComputeUplinkMIC( - SNwkSIntKey, - FNwkSIntKey, - 0x24, - 3, - 2, - DevAddr, - 0x42, - test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte)), - ).([4]byte) - msg.Payload.MIC = mic[:] - - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + RxMetadata: MakeRxMetadataSlice(), + } + } - return msg - }(), - }, - { - "1.1/unconfirmed/no ack/FCnt resets", - &ttnpb.EndDevice{ + handleUplinkErrCh := handle(ctx, makeMsg(false)) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.HaveSameErrorDefinitionAs, ErrDecodePayload) + }) + }, + }, + + { + Name: "Join-request/Get fail", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCorrelationIDs := events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Error: errTest, + } + } + + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, makeJoinRequest, start, duplicateCount) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.EqualErrorOrDefinition, errTest) + }) + }, + }, + + { + Name: "Join-request/Get ABP device", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0, + LoRaWANVersion: ttnpb.MAC_V1_0, + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var reqCtx context.Context + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs := events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtDropJoinRequest(reqCtx, makeOTAAIdentifiers(nil), ErrABPJoinRequest)) + }), should.BeTrue) { + return false + } + + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, makeJoinRequest, start, duplicateCount) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.EqualErrorOrDefinition, ErrABPJoinRequest) + }) + }, + }, + + { + Name: "Join-request/Get multicast device", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0, + LoRaWANVersion: ttnpb.MAC_V1_0, + Multicast: true, + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var reqCtx context.Context + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs := events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtDropJoinRequest(reqCtx, makeOTAAIdentifiers(nil), ErrABPJoinRequest)) + }), should.BeTrue) { + return false + } + + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, makeJoinRequest, start, duplicateCount) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.EqualErrorOrDefinition, ErrABPJoinRequest) + }) + }, + }, + + { + Name: "Join-request/Get OTAA device/1.0.2/JS fail", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, MACSettings: &ttnpb.MACSettings{ - ResetsFCnt: &pbtypes.BoolValue{Value: true}, + Rx1Delay: &ttnpb.MACSettings_RxDelayValue{ + Value: ttnpb.RX_DELAY_3, + }, }, - MACState: &ttnpb.MACState{ - LoRaWANVersion: ttnpb.MAC_V1_1, - CurrentParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, + SupportsJoin: true, + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var reqCtx context.Context + var reqCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } + + if !a.So(AssertNsJsPeerHandleAuthJoinRequest(ctx, env.Cluster.GetPeer, env.Cluster.Auth, + func(ctx context.Context, ids ttnpb.Identifiers) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) + }, + func(ctx context.Context, req *ttnpb.JoinRequest) bool { + return a.So(req.CorrelationIDs, should.HaveSameElementsDeep, reqCorrelationIDs) && + a.So(req.DevAddr, should.NotBeEmpty) && + a.So(req.DevAddr.NwkID(), should.Resemble, netID.ID()) && + a.So(req.DevAddr.NetIDType(), should.Equal, netID.Type()) && + a.So(req, should.Resemble, &ttnpb.JoinRequest{ + CFList: &ttnpb.CFList{ + Type: ttnpb.CFListType_FREQUENCIES, + Freq: []uint32{8671000, 8673000, 8675000, 8677000, 8679000}, }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, + CorrelationIDs: req.CorrelationIDs, + DevAddr: req.DevAddr, + NetID: netID, + RawPayload: msg.RawPayload, + RxDelay: ttnpb.RX_DELAY_3, + SelectedMACVersion: ttnpb.MAC_V1_0_2, + }) + }, + &grpc.EmptyCallOption{}, + NsJsHandleJoinResponse{ + Error: errTest, + }, + ), should.BeTrue) { + return false + } + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + if !a.So(ev.Data(), should.BeError) { + return false + } + err, ok := errors.From(ev.Data().(error)) + if !a.So(ok, should.BeTrue) { + return false + } + return a.So(ev, should.ResembleEvent, EvtDropJoinRequest(reqCtx, makeOTAAIdentifiers(nil), err)) + }), should.BeTrue) { + return false + } + + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, makeJoinRequest, start, duplicateCount) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeError) + }) + }, + }, + + { + Name: "Join-request/Get OTAA device/1.1/JS fail", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACSettings: &ttnpb.MACSettings{ + Rx1Delay: &ttnpb.MACSettings_RxDelayValue{ + Value: ttnpb.RX_DELAY_3, }, - DesiredParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, + }, + SupportsJoin: true, + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var reqCtx context.Context + var reqCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } + + if !a.So(AssertNsJsPeerHandleAuthJoinRequest(ctx, env.Cluster.GetPeer, env.Cluster.Auth, + func(ctx context.Context, ids ttnpb.Identifiers) bool { + return a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) + }, + func(ctx context.Context, req *ttnpb.JoinRequest) bool { + return a.So(req.CorrelationIDs, should.HaveSameElementsDeep, reqCorrelationIDs) && + a.So(req.DevAddr, should.NotBeEmpty) && + a.So(req.DevAddr.NwkID(), should.Resemble, netID.ID()) && + a.So(req.DevAddr.NetIDType(), should.Equal, netID.Type()) && + a.So(req, should.Resemble, &ttnpb.JoinRequest{ + CFList: &ttnpb.CFList{ + Type: ttnpb.CFListType_FREQUENCIES, + Freq: []uint32{8671000, 8673000, 8675000, 8677000, 8679000}, }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, + CorrelationIDs: req.CorrelationIDs, + DevAddr: req.DevAddr, + DownlinkSettings: ttnpb.DLSettings{ + OptNeg: true, }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, - }, + NetID: netID, + RawPayload: msg.RawPayload, + RxDelay: ttnpb.RX_DELAY_3, + SelectedMACVersion: ttnpb.MAC_V1_1, + }) }, - LoRaWANVersion: ttnpb.MAC_V1_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ - ApplicationID: ApplicationID, - }, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, + &grpc.EmptyCallOption{}, + NsJsHandleJoinResponse{ + Error: errTest, }, - FrequencyPlanID: test.EUFrequencyPlanID, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - LastFCntUp: 0x42424249, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - SNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &SNwkSIntKey, - }, - NwkSEncKey: &ttnpb.KeyEnvelope{ - Key: &NwkSEncKey, - }, + ), should.BeTrue) { + return false + } + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + if !a.So(ev.Data(), should.BeError) { + return false + } + err, ok := errors.From(ev.Data().(error)) + if !a.So(ok, should.BeTrue) { + return false + } + return a.So(ev, should.ResembleEvent, EvtDropJoinRequest(reqCtx, makeOTAAIdentifiers(nil), err)) + }), should.BeTrue) { + return false + } + + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, makeJoinRequest, start, duplicateCount) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeError) + }) + }, + }, + { + Name: "Join-request/Get OTAA device/1.1/JS not found", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACSettings: &ttnpb.MACSettings{ + Rx1Delay: &ttnpb.MACSettings_RxDelayValue{ + Value: ttnpb.RX_DELAY_3, }, }, + SupportsJoin: true, + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var reqCtx context.Context + var reqCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } + + if !a.So(test.AssertClusterGetPeerRequest(ctx, env.Cluster.GetPeer, func(ctx context.Context, role ttnpb.PeerInfo_Role, ids ttnpb.Identifiers) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(role, should.Equal, ttnpb.PeerInfo_JOIN_SERVER) && + a.So(ids, should.Resemble, getDevice.EndDeviceIdentifiers) }, - 0x42, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, false) - msg.Payload.GetMACPayload().FHDR.Ack = false - msg.Payload.GetMACPayload().FHDR.ADR = false - msg.Settings.Frequency = 100045 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate - - pld := msg.Payload.GetMACPayload() - pld.DevAddr = DevAddr - pld.FCnt = 0x42 - - msg.Payload.MIC = nil - mic := test.Must(crypto.ComputeUplinkMIC( - SNwkSIntKey, - FNwkSIntKey, - 0, - 3, - 2, - DevAddr, - 0x42, - test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte)), - ).([4]byte) - msg.Payload.MIC = mic[:] - - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + nil, + ), should.BeTrue) { + return false + } - return msg - }(), + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + if !a.So(ev.Data(), should.BeError) { + return false + } + err, ok := errors.From(ev.Data().(error)) + if !a.So(ok, should.BeTrue) { + return false + } + return a.So(ev, should.ResembleEvent, EvtDropJoinRequest(reqCtx, makeOTAAIdentifiers(nil), err)) + }), should.BeTrue) { + return false + } + + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, makeJoinRequest, start, duplicateCount) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeError) + }) }, - { - "1.1/confirmed/ack/FCnt resets", - &ttnpb.EndDevice{ + }, + + { + Name: "Join-request/Get OTAA device/1.0.2/JS accept/Set fail", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, MACSettings: &ttnpb.MACSettings{ - ResetsFCnt: &pbtypes.BoolValue{Value: true}, + DesiredRx1Delay: &ttnpb.MACSettings_RxDelayValue{ + Value: ttnpb.RX_DELAY_3, + }, }, - MACState: &ttnpb.MACState{ - LoRaWANVersion: ttnpb.MAC_V1_1, - PendingApplicationDownlink: ttnpb.NewPopulatedApplicationDownlink(test.Randy, false), - CurrentParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(33, true), + }, + SupportsJoin: true, + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var reqCtx context.Context + var reqCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } + + joinResp := makeJoinResponse(ttnpb.MAC_V1_0_2) + + var joinReq *ttnpb.JoinRequest + if !a.So(AssertNsJsPeerHandleAuthJoinRequest(ctx, env.Cluster.GetPeer, env.Cluster.Auth, + func(ctx context.Context, ids ttnpb.Identifiers) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) + }, + func(ctx context.Context, req *ttnpb.JoinRequest) bool { + joinReq = req + return a.So(req.CorrelationIDs, should.HaveSameElementsDeep, reqCorrelationIDs) && + a.So(req.DevAddr, should.NotBeEmpty) && + a.So(req.DevAddr.NwkID(), should.Resemble, netID.ID()) && + a.So(req.DevAddr.NetIDType(), should.Equal, netID.Type()) && + a.So(req, should.Resemble, &ttnpb.JoinRequest{ + CFList: &ttnpb.CFList{ + Type: ttnpb.CFListType_FREQUENCIES, + Freq: []uint32{8671000, 8673000, 8675000, 8677000, 8679000}, }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, + CorrelationIDs: req.CorrelationIDs, + DevAddr: req.DevAddr, + NetID: netID, + RawPayload: msg.RawPayload, + RxDelay: ttnpb.RX_DELAY_3, + SelectedMACVersion: ttnpb.MAC_V1_0_2, + }) + }, + &grpc.EmptyCallOption{}, + NsJsHandleJoinResponse{ + Response: joinResp, + }, + ), should.BeTrue) { + return false + } + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtForwardJoinRequest(reqCtx, makeOTAAIdentifiers(nil), nil)) + }), should.BeTrue) { + return false + } + + mds := sendUplinkDuplicates(ctx, handle, env.DeduplicationDone, makeJoinRequest, start, duplicateCount) + mds = append(mds, msg.RxMetadata...) + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtMergeMetadata(reqCtx, makeOTAAIdentifiers(nil), len(mds))) + }), should.BeTrue) { + return false + } + + var recentUp *ttnpb.UplinkMessage + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.SetByID to be called") + return false + + case req := <-env.DeviceRegistry.SetByID: + a.So(req.Context, should.HaveParentContextOrEqual, reqCtx) + a.So(req.ApplicationIdentifiers, should.Resemble, appID) + a.So(req.DeviceID, should.Resemble, devID) + a.So(req.Paths, should.HaveSameElementsDeep, joinSetByEUIGetPaths[:]) + dev, sets, err := req.Func(&ttnpb.EndDevice{ + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(33, true), }, - DesiredParameters: ttnpb.MACParameters{ - ADRAckLimit: 1, - ADRAckDelay: 1, - ADRNbTrans: 1, - Rx2Frequency: 100000, - Channels: []*ttnpb.MACParameters_Channel{ - { - UplinkFrequency: 100042, - DownlinkFrequency: 100043, - MinDataRateIndex: 1, - MaxDataRateIndex: 4, - }, - { - UplinkFrequency: 868300000, - DownlinkFrequency: 868300001, - MinDataRateIndex: 0, - MaxDataRateIndex: 5, - }, - { - UplinkFrequency: 100045, - DownlinkFrequency: 100046, - MinDataRateIndex: 3, - MaxDataRateIndex: 4, - }, - ttnpb.NewPopulatedMACParameters_Channel(test.Randy, false), - }, + QueuedApplicationDownlinks: []*ttnpb.ApplicationDownlink{ + makeApplicationDownlink(), }, - }, - LoRaWANVersion: ttnpb.MAC_V1_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ - ApplicationID: ApplicationID, + }) + if !a.So(err, should.BeNil) || !a.So(dev, should.NotBeNil) { + return false + } + a.So(sets, should.HaveSameElementsDeep, joinSetByEUISetPaths[:]) + + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_0_2) + macState.DesiredParameters.Rx1Delay = ttnpb.RX_DELAY_3 + macState.CurrentParameters.Rx1Delay = macState.DesiredParameters.Rx1Delay + macState.CurrentParameters.Channels = macState.DesiredParameters.Channels + macState.RxWindowsAvailable = true + macState.QueuedJoinAccept = &ttnpb.MACState_JoinAccept{ + Keys: *makeSessionKeys(ttnpb.MAC_V1_0_2), + Payload: joinResp.RawPayload, + Request: *joinReq, + } + a.So(dev.PendingMACState, should.Resemble, macState) + a.So(dev.QueuedApplicationDownlinks, should.BeNil) + if a.So(dev.RecentUplinks, should.NotBeEmpty) { + recentUp = dev.RecentUplinks[len(dev.RecentUplinks)-1] + a.So([]time.Time{start, recentUp.ReceivedAt, time.Now()}, should.BeChronological) + a.So(recentUp.RxMetadata, should.HaveSameElementsDiff, mds) + expectedUp := makeJoinRequest(true) + expectedUp.CorrelationIDs = reqCorrelationIDs + expectedUp.DeviceChannelIndex = 2 + expectedUp.ReceivedAt = recentUp.ReceivedAt + expectedUp.RxMetadata = recentUp.RxMetadata + expectedUp.Settings.DataRateIndex = ttnpb.DATA_RATE_1 + a.So(dev.RecentUplinks, should.HaveEmptyDiff, append(CopyUplinkMessages(getDevice.RecentUplinks...), expectedUp)) + } + req.Response <- DeviceRegistrySetByIDResponse{ + Error: errTest, + } + } + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + a.So(ev, should.ResembleEvent, EvtDropJoinRequest(reqCtx, makeOTAAIdentifiers(nil), errTest)) + return true + }), should.BeTrue) { + return false + } + + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, func(decoded bool) *ttnpb.UplinkMessage { + msg := makeJoinRequest(decoded) + if !decoded { + return msg + } + msg.DeviceChannelIndex = 2 + msg.Settings.DataRateIndex = ttnpb.DATA_RATE_1 + return msg + }, start, duplicateCount) + + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.EqualErrorOrDefinition, errTest) + }) + }, + }, + + { + Name: "Join-request/Get OTAA device/1.1/JS accept/Set success/Downlink add fail", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACSettings: &ttnpb.MACSettings{ + DesiredRx1Delay: &ttnpb.MACSettings_RxDelayValue{ + Value: ttnpb.RX_DELAY_3, }, - DeviceID: DeviceID, - DevAddr: &DevAddr, - JoinEUI: &JoinEUI, - DevEUI: &DevEUI, }, - FrequencyPlanID: test.EUFrequencyPlanID, - Session: &ttnpb.Session{ - DevAddr: DevAddr, - LastFCntUp: 0x42424249, - LastConfFCntDown: 0x24, - SessionKeys: ttnpb.SessionKeys{ - FNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &FNwkSIntKey, - }, - SNwkSIntKey: &ttnpb.KeyEnvelope{ - Key: &SNwkSIntKey, - }, - NwkSEncKey: &ttnpb.KeyEnvelope{ - Key: &NwkSEncKey, - }, - }, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeDataUplink(33, true), }, - RecentDownlinks: []*ttnpb.DownlinkMessage{ - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - ttnpb.NewPopulatedDownlinkMessage(test.Randy, false), - func() *ttnpb.DownlinkMessage { - msg := ttnpb.NewPopulatedDownlinkMessage(test.Randy, false) - msg.Payload.GetMACPayload().FCnt = 0x24 - return msg - }(), + SupportsJoin: true, + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var reqCtx context.Context + var reqCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } + + joinResp := makeJoinResponse(ttnpb.MAC_V1_1) + + var joinReq *ttnpb.JoinRequest + if !a.So(AssertNsJsPeerHandleAuthJoinRequest(ctx, env.Cluster.GetPeer, env.Cluster.Auth, + func(ctx context.Context, ids ttnpb.Identifiers) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) }, + func(ctx context.Context, req *ttnpb.JoinRequest) bool { + joinReq = req + return a.So(req.CorrelationIDs, should.HaveSameElementsDeep, reqCorrelationIDs) && + a.So(req.DevAddr, should.NotBeEmpty) && + a.So(req.DevAddr.NwkID(), should.Resemble, netID.ID()) && + a.So(req.DevAddr.NetIDType(), should.Equal, netID.Type()) && + a.So(req, should.Resemble, &ttnpb.JoinRequest{ + CFList: &ttnpb.CFList{ + Type: ttnpb.CFListType_FREQUENCIES, + Freq: []uint32{8671000, 8673000, 8675000, 8677000, 8679000}, + }, + CorrelationIDs: req.CorrelationIDs, + DevAddr: req.DevAddr, + DownlinkSettings: ttnpb.DLSettings{ + OptNeg: true, + }, + NetID: netID, + RawPayload: msg.RawPayload, + RxDelay: ttnpb.RX_DELAY_3, + SelectedMACVersion: ttnpb.MAC_V1_1, + }) + }, + &grpc.EmptyCallOption{}, + NsJsHandleJoinResponse{ + Response: joinResp, + }, + ), should.BeTrue) { + return false + } + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtForwardJoinRequest(reqCtx, makeOTAAIdentifiers(nil), nil)) + }), should.BeTrue) { + return false + } + + mds := sendUplinkDuplicates(ctx, handle, env.DeduplicationDone, makeJoinRequest, start, duplicateCount) + mds = append(mds, msg.RxMetadata...) + + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtMergeMetadata(reqCtx, makeOTAAIdentifiers(nil), len(mds))) + }), should.BeTrue) { + return false + } + + var recentUp *ttnpb.UplinkMessage + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.SetByID to be called") + return false + + case req := <-env.DeviceRegistry.SetByID: + a.So(req.Context, should.HaveParentContextOrEqual, reqCtx) + a.So(req.ApplicationIdentifiers, should.Resemble, appID) + a.So(req.DeviceID, should.Resemble, devID) + a.So(req.Paths, should.HaveSameElementsDeep, joinSetByEUIGetPaths[:]) + dev, sets, err := req.Func(&ttnpb.EndDevice{ + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeDataUplink(33, true), + }, + QueuedApplicationDownlinks: []*ttnpb.ApplicationDownlink{ + makeApplicationDownlink(), + }, + }) + if !a.So(err, should.BeNil) || !a.So(dev, should.NotBeNil) { + return false + } + a.So(sets, should.HaveSameElementsDeep, joinSetByEUISetPaths[:]) + + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1) + macState.DesiredParameters.Rx1Delay = ttnpb.RX_DELAY_3 + macState.CurrentParameters.Rx1Delay = macState.DesiredParameters.Rx1Delay + macState.CurrentParameters.Channels = macState.DesiredParameters.Channels + macState.RxWindowsAvailable = true + macState.QueuedJoinAccept = &ttnpb.MACState_JoinAccept{ + Keys: *makeSessionKeys(ttnpb.MAC_V1_1), + Payload: joinResp.RawPayload, + Request: *joinReq, + } + a.So(dev.PendingMACState, should.Resemble, macState) + a.So(dev.QueuedApplicationDownlinks, should.BeNil) + if a.So(dev.RecentUplinks, should.NotBeEmpty) { + recentUp = dev.RecentUplinks[len(dev.RecentUplinks)-1] + a.So([]time.Time{start, recentUp.ReceivedAt, time.Now()}, should.BeChronological) + a.So(recentUp.RxMetadata, should.HaveSameElementsDiff, mds) + expectedUp := makeJoinRequest(true) + expectedUp.CorrelationIDs = reqCorrelationIDs + expectedUp.DeviceChannelIndex = 2 + expectedUp.ReceivedAt = recentUp.ReceivedAt + expectedUp.RxMetadata = recentUp.RxMetadata + expectedUp.Settings.DataRateIndex = ttnpb.DATA_RATE_1 + a.So(dev.RecentUplinks, should.HaveEmptyDiff, append(CopyUplinkMessages(getDevice.RecentUplinks...), expectedUp)) + } + req.Response <- DeviceRegistrySetByIDResponse{ + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + PendingMACState: macState, + QueuedApplicationDownlinks: dev.QueuedApplicationDownlinks, + RecentUplinks: dev.RecentUplinks, + CreatedAt: start, + UpdatedAt: time.Now(), + }, + } + } + + if !a.So(AssertDownlinkTaskAddRequest(ctx, env.DownlinkTasks.Add, func(ctx context.Context, ids ttnpb.EndDeviceIdentifiers, startAt time.Time, replace bool) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) && + a.So(startAt, should.Resemble, recentUp.ReceivedAt.Add(5*time.Second-NSScheduleWindow())) && + a.So(replace, should.BeTrue) }, - 0x42, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageUplink(test.Randy, SNwkSIntKey, FNwkSIntKey, true) - msg.Payload.GetMACPayload().FHDR.Ack = true - msg.Payload.GetMACPayload().FHDR.ADR = false - msg.Settings.Frequency = 100045 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate - - pld := msg.Payload.GetMACPayload() - pld.DevAddr = DevAddr - pld.FCnt = 0x42 - - msg.Payload.MIC = nil - mic := test.Must(crypto.ComputeUplinkMIC( - SNwkSIntKey, - FNwkSIntKey, - 0x24, - 3, - 2, - DevAddr, - 0x42, - test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte)), - ).([4]byte) - msg.Payload.MIC = mic[:] - - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + errTest, + ), should.BeTrue) { + return false + } + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, func(decoded bool) *ttnpb.UplinkMessage { + msg := makeJoinRequest(decoded) + if !decoded { + return msg + } + msg.DeviceChannelIndex = 2 + msg.Settings.DataRateIndex = ttnpb.DATA_RATE_1 return msg - }(), + }, start, duplicateCount) + + if !assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeNil) + }) { + return false + } + + if asRecvCh != nil { + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for NetworkServer.handleASUplink to be called") + return false + + case req := <-asRecvCh: + a.So(req.Uplink, should.Resemble, &ttnpb.ApplicationUp{ + CorrelationIDs: reqCorrelationIDs, + EndDeviceIdentifiers: *makeOTAAIdentifiers(&joinReq.DevAddr), + Up: &ttnpb.ApplicationUp_JoinAccept{JoinAccept: &ttnpb.ApplicationJoinAccept{ + AppSKey: makeSessionKeys(ttnpb.MAC_V1_1).AppSKey, + InvalidatedDownlinks: []*ttnpb.ApplicationDownlink{ + makeApplicationDownlink(), + }, + SessionKeyID: makeSessionKeys(ttnpb.MAC_V1_1).SessionKeyID, + }}, + }) + req.Response <- ttnpb.Empty + } + } + return true }, - } { - t.Run(tc.Name, func(t *testing.T) { + }, + + { + Name: "Join-request/Get OTAA device/1.0.2/JS accept/Set success/Downlink add success", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) a := assertions.New(t) - authorizedCtx := test.ContextWithT(authorizedCtx, t) + start := time.Now() - redisClient, flush := test.NewRedis(t, "networkserver_test") - defer flush() - defer redisClient.Close() - devReg := &redis.DeviceRegistry{Redis: redisClient} + msg := makeJoinRequest(false) - populateSessionKeys := func(s *ttnpb.Session) { - for s.SessionKeys.FNwkSIntKey == nil || - s.SessionKeys.FNwkSIntKey.Key.IsZero() || - s.SessionKeys.FNwkSIntKey.KEKLabel == "" && *s.SessionKeys.FNwkSIntKey.Key == FNwkSIntKey { + handleUplinkErrCh := handle(ctx, msg) - s.SessionKeys.FNwkSIntKey = ttnpb.NewPopulatedKeyEnvelope(test.Randy, false) + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, + MACSettings: &ttnpb.MACSettings{ + DesiredRx1Delay: &ttnpb.MACSettings_RxDelayValue{ + Value: ttnpb.RX_DELAY_3, + }, + }, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(33, true), + }, + SupportsJoin: true, + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var reqCtx context.Context + var reqCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } - for s.SessionKeys.SNwkSIntKey == nil || - s.SessionKeys.SNwkSIntKey.Key.IsZero() || - s.SessionKeys.SNwkSIntKey.KEKLabel == "" && *s.SessionKeys.SNwkSIntKey.Key == SNwkSIntKey { + joinResp := makeJoinResponse(ttnpb.MAC_V1_0_2) - s.SessionKeys.SNwkSIntKey = ttnpb.NewPopulatedKeyEnvelope(test.Randy, false) - } + var joinReq *ttnpb.JoinRequest + if !a.So(AssertNsJsPeerHandleAuthJoinRequest(ctx, env.Cluster.GetPeer, env.Cluster.Auth, + func(ctx context.Context, ids ttnpb.Identifiers) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) + }, + func(ctx context.Context, req *ttnpb.JoinRequest) bool { + joinReq = req + return a.So(req.CorrelationIDs, should.HaveSameElementsDeep, reqCorrelationIDs) && + a.So(req.DevAddr, should.NotBeEmpty) && + a.So(req.DevAddr.NwkID(), should.Resemble, netID.ID()) && + a.So(req.DevAddr.NetIDType(), should.Equal, netID.Type()) && + a.So(req, should.Resemble, &ttnpb.JoinRequest{ + CFList: &ttnpb.CFList{ + Type: ttnpb.CFListType_FREQUENCIES, + Freq: []uint32{8671000, 8673000, 8675000, 8677000, 8679000}, + }, + CorrelationIDs: req.CorrelationIDs, + DevAddr: req.DevAddr, + NetID: netID, + RawPayload: msg.RawPayload, + RxDelay: ttnpb.RX_DELAY_3, + SelectedMACVersion: ttnpb.MAC_V1_0_2, + }) + }, + &grpc.EmptyCallOption{}, + NsJsHandleJoinResponse{ + Response: joinResp, + }, + ), should.BeTrue) { + return false } - ctx := context.WithValue(authorizedCtx, struct{}{}, 42) - ctx = log.NewContext(ctx, test.GetLogger(t)) + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtForwardJoinRequest(reqCtx, makeOTAAIdentifiers(nil), nil)) + }), should.BeTrue) { + return false + } - // Fill DeviceRegistry with devices - for i := 0; i < DeviceCount; i++ { - pb := &ttnpb.EndDevice{} - if err := pb.SetFields(ttnpb.NewPopulatedEndDevice(test.Randy, false), []string{ - "frequency_plan_id", - "ids", - "lorawan_phy_version", - "lorawan_version", - "mac_state", - "multicast", - "pending_session", - "session", - }...); err != nil { - t.Fatalf("Failed to set fields: %s", err) - } - for unique.ID(ctx, pb.EndDeviceIdentifiers) == unique.ID(ctx, tc.Device.EndDeviceIdentifiers) { - if err := pb.SetFields(ttnpb.NewPopulatedEndDevice(test.Randy, false), []string{ - "frequency_plan_id", - "ids", - "lorawan_phy_version", - "lorawan_version", - "mac_state", - "multicast", - "pending_session", - "session", - }...); err != nil { - t.Fatalf("Failed to set fields: %s", err) - } - } + mds := sendUplinkDuplicates(ctx, handle, env.DeduplicationDone, makeJoinRequest, start, duplicateCount) + mds = append(mds, msg.RxMetadata...) - if s := pb.Session; s != nil { - populateSessionKeys(s) + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtMergeMetadata(reqCtx, makeOTAAIdentifiers(nil), len(mds))) + }), should.BeTrue) { + return false + } - s.DevAddr = DevAddr - pb.EndDeviceIdentifiers.DevAddr = &s.DevAddr - for pb.PendingSession != nil && pb.PendingSession.DevAddr.Equal(s.DevAddr) { - pb.PendingSession.DevAddr = *types.NewPopulatedDevAddr(test.Randy) - } - } else if s := pb.PendingSession; s != nil { - populateSessionKeys(s) + var recentUp *ttnpb.UplinkMessage + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.SetByID to be called") + return false + + case req := <-env.DeviceRegistry.SetByID: + a.So(req.Context, should.HaveParentContextOrEqual, reqCtx) + a.So(req.ApplicationIdentifiers, should.Resemble, appID) + a.So(req.DeviceID, should.Resemble, devID) + a.So(req.Paths, should.HaveSameElementsDeep, joinSetByEUIGetPaths[:]) + dev, sets, err := req.Func(&ttnpb.EndDevice{ + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(33, true), + }, + QueuedApplicationDownlinks: []*ttnpb.ApplicationDownlink{ + makeApplicationDownlink(), + }, + }) + if !a.So(err, should.BeNil) || !a.So(dev, should.NotBeNil) { + return false + } + a.So(sets, should.HaveSameElementsDeep, joinSetByEUISetPaths[:]) + + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_0_2) + macState.DesiredParameters.Rx1Delay = ttnpb.RX_DELAY_3 + macState.CurrentParameters.Rx1Delay = macState.DesiredParameters.Rx1Delay + macState.CurrentParameters.Channels = macState.DesiredParameters.Channels + macState.RxWindowsAvailable = true + macState.QueuedJoinAccept = &ttnpb.MACState_JoinAccept{ + Keys: *makeSessionKeys(ttnpb.MAC_V1_0_2), + Payload: joinResp.RawPayload, + Request: *joinReq, + } + a.So(dev.PendingMACState, should.Resemble, macState) + a.So(dev.QueuedApplicationDownlinks, should.BeNil) + if a.So(dev.RecentUplinks, should.NotBeEmpty) { + recentUp = dev.RecentUplinks[len(dev.RecentUplinks)-1] + a.So([]time.Time{start, recentUp.ReceivedAt, time.Now()}, should.BeChronological) + a.So(recentUp.RxMetadata, should.HaveSameElementsDiff, mds) + expectedUp := makeJoinRequest(true) + expectedUp.CorrelationIDs = reqCorrelationIDs + expectedUp.DeviceChannelIndex = 2 + expectedUp.ReceivedAt = recentUp.ReceivedAt + expectedUp.RxMetadata = recentUp.RxMetadata + expectedUp.Settings.DataRateIndex = ttnpb.DATA_RATE_1 + a.So(dev.RecentUplinks, should.HaveEmptyDiff, append(CopyUplinkMessages(getDevice.RecentUplinks...), expectedUp)) + } + req.Response <- DeviceRegistrySetByIDResponse{ + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + PendingMACState: macState, + QueuedApplicationDownlinks: dev.QueuedApplicationDownlinks, + RecentUplinks: dev.RecentUplinks, + CreatedAt: start, + UpdatedAt: time.Now(), + }, + } + } - s.DevAddr = DevAddr - for pb.Session != nil && pb.Session.DevAddr.Equal(s.DevAddr) { - pb.Session.DevAddr = *types.NewPopulatedDevAddr(test.Randy) - pb.EndDeviceIdentifiers.DevAddr = &pb.Session.DevAddr - } + if !a.So(AssertDownlinkTaskAddRequest(ctx, env.DownlinkTasks.Add, func(ctx context.Context, ids ttnpb.EndDeviceIdentifiers, startAt time.Time, replace bool) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) && + a.So(startAt, should.Resemble, recentUp.ReceivedAt.Add(5*time.Second-NSScheduleWindow())) && + a.So(replace, should.BeTrue) + }, + nil, + ), should.BeTrue) { + return false + } + + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, func(decoded bool) *ttnpb.UplinkMessage { + msg := makeJoinRequest(decoded) + if !decoded { + return msg } + msg.DeviceChannelIndex = 2 + msg.Settings.DataRateIndex = ttnpb.DATA_RATE_1 + return msg + }, start, duplicateCount) - ret, err := devReg.SetByID(authorizedCtx, pb.ApplicationIdentifiers, pb.DeviceID, - []string{ - "created_at", - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "mac_state", - "multicast", - "pending_session", - "session", - "updated_at", - }, - func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { - if !a.So(stored, should.BeNil) { - t.Fatal("Registry is not empty") - } - return CopyEndDevice(pb), []string{ - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "mac_state", - "multicast", - "pending_session", - "session", - }, nil + if !assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeNil) + }) { + return false + } + + if asRecvCh != nil { + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for NetworkServer.handleASUplink to be called") + return false + + case req := <-asRecvCh: + a.So(req.Uplink, should.Resemble, &ttnpb.ApplicationUp{ + CorrelationIDs: reqCorrelationIDs, + EndDeviceIdentifiers: *makeOTAAIdentifiers(&joinReq.DevAddr), + Up: &ttnpb.ApplicationUp_JoinAccept{JoinAccept: &ttnpb.ApplicationJoinAccept{ + AppSKey: makeSessionKeys(ttnpb.MAC_V1_0_2).AppSKey, + InvalidatedDownlinks: []*ttnpb.ApplicationDownlink{ + makeApplicationDownlink(), + }, + SessionKeyID: makeSessionKeys(ttnpb.MAC_V1_0_2).SessionKeyID, + }}, }) - if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) { - t.Fatalf("Failed to create device, error: %s", err) - } - pb.CreatedAt = ret.CreatedAt - pb.UpdatedAt = ret.UpdatedAt - a.So(ret, should.Resemble, pb) - } - - deduplicationDoneCh := make(chan WindowEndRequest, 1) - - collectionDoneCh := make(chan WindowEndRequest, 1) - - type asSendReq struct { - up *ttnpb.ApplicationUp - errch chan error - } - asSendCh := make(chan asSendReq) - - type downlinkTasksAddRequest struct { - ctx context.Context - devID ttnpb.EndDeviceIdentifiers - t time.Time - replace bool - } - downlinkAddCh := make(chan downlinkTasksAddRequest, 1) - - ns := test.Must(New( - component.MustNew(test.GetLogger(t), &component.Config{}), - &Config{ - Devices: devReg, - DeduplicationWindow: 42, - CooldownWindow: 42, - DownlinkTasks: &MockDownlinkTaskQueue{ - PopFunc: DownlinkTaskPopBlockFunc, - AddFunc: func(ctx context.Context, devID ttnpb.EndDeviceIdentifiers, t time.Time, replace bool) error { - downlinkAddCh <- downlinkTasksAddRequest{ - ctx: ctx, - devID: devID, - t: t, - replace: replace, - } - return nil - }, + req.Response <- ttnpb.Empty + } + } + return true + }, + }, + + { + Name: "Join-request/Get OTAA device/1.1/JS accept/Set success/Downlink add success", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) + + start := time.Now() + + msg := makeJoinRequest(false) + + handleUplinkErrCh := handle(ctx, msg) + + getDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACSettings: &ttnpb.MACSettings{ + DesiredRx1Delay: &ttnpb.MACSettings_RxDelayValue{ + Value: ttnpb.RX_DELAY_3, }, }, - WithCollectionDoneFunc(MakeWindowEndChFunc(collectionDoneCh)), - WithDeduplicationDoneFunc(MakeWindowEndChFunc(deduplicationDoneCh)), - WithASUplinkHandler(func(ctx context.Context, ids ttnpb.ApplicationIdentifiers, up *ttnpb.ApplicationUp) (bool, error) { - req := asSendReq{ - up: up, - errch: make(chan error), - } - asSendCh <- req - return true, <-req.errch - }), - )).(*NetworkServer) - ns.FrequencyPlans = frequencyplans.NewStore(test.FrequencyPlansFetcher) - test.Must(nil, ns.Start()) - defer ns.Close() + RecentUplinks: []*ttnpb.UplinkMessage{ + makeDataUplink(33, true), + }, + SupportsJoin: true, + CreatedAt: start, + UpdatedAt: time.Now(), + } - pb := CopyEndDevice(tc.Device) + var reqCtx context.Context + var reqCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.GetByEUI to be called") + return false + + case req := <-env.DeviceRegistry.GetByEUI: + reqCtx = req.Context + reqCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(reqCorrelationIDs, should.Contain, id) + } + a.So(reqCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.JoinEUI, should.Resemble, joinEUI) + a.So(req.DevEUI, should.Resemble, devEUI) + a.So(req.Paths, should.HaveSameElementsDeep, joinGetByEUIPaths[:]) + req.Response <- DeviceRegistryGetByEUIResponse{ + Device: CopyEndDevice(getDevice), + } + } - start := time.Now() + joinResp := makeJoinResponse(ttnpb.MAC_V1_1) - ret, err := devReg.SetByID(authorizedCtx, pb.ApplicationIdentifiers, pb.DeviceID, - []string{ - "created_at", - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "mac_settings", - "mac_state", - "multicast", - "pending_session", - "recent_downlinks", - "session", - "updated_at", + var joinReq *ttnpb.JoinRequest + if !a.So(AssertNsJsPeerHandleAuthJoinRequest(ctx, env.Cluster.GetPeer, env.Cluster.Auth, + func(ctx context.Context, ids ttnpb.Identifiers) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) }, - func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { - if !a.So(stored, should.BeNil) { - t.Fatal("Registry is not empty") - } - return CopyEndDevice(tc.Device), []string{ - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "mac_settings", - "mac_state", - "multicast", - "pending_session", - "recent_downlinks", - "session", - }, nil - }) - if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) { - t.Fatalf("Failed to create device, error: %s", err) + func(ctx context.Context, req *ttnpb.JoinRequest) bool { + joinReq = req + return a.So(req.CorrelationIDs, should.HaveSameElementsDeep, reqCorrelationIDs) && + a.So(req.DevAddr, should.NotBeEmpty) && + a.So(req.DevAddr.NwkID(), should.Resemble, netID.ID()) && + a.So(req.DevAddr.NetIDType(), should.Equal, netID.Type()) && + a.So(req, should.Resemble, &ttnpb.JoinRequest{ + CFList: &ttnpb.CFList{ + Type: ttnpb.CFListType_FREQUENCIES, + Freq: []uint32{8671000, 8673000, 8675000, 8677000, 8679000}, + }, + CorrelationIDs: req.CorrelationIDs, + DevAddr: req.DevAddr, + DownlinkSettings: ttnpb.DLSettings{ + OptNeg: true, + }, + NetID: netID, + RawPayload: msg.RawPayload, + RxDelay: ttnpb.RX_DELAY_3, + SelectedMACVersion: ttnpb.MAC_V1_1, + }) + }, + &grpc.EmptyCallOption{}, + NsJsHandleJoinResponse{ + Response: joinResp, + }, + ), should.BeTrue) { + return false } - pb.CreatedAt = ret.CreatedAt - a.So(ret.UpdatedAt, should.HappenAfter, start) - pb.UpdatedAt = ret.UpdatedAt - a.So(ret, should.HaveEmptyDiff, pb) - errch := make(chan error, 1) - go func() { - _, err := ns.HandleUplink(ctx, CopyUplinkMessage(tc.UplinkMessage)) - errch <- err - }() + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtForwardJoinRequest(reqCtx, makeOTAAIdentifiers(nil), nil)) + }), should.BeTrue) { + return false + } - md := sendUplinkDuplicates(t, ns, deduplicationDoneCh, ctx, tc.UplinkMessage, DuplicateCount) - close(deduplicationDoneCh) + mds := sendUplinkDuplicates(ctx, handle, env.DeduplicationDone, makeJoinRequest, start, duplicateCount) + mds = append(mds, msg.RxMetadata...) - if pb.MACState != nil && pb.MACState.PendingApplicationDownlink != nil { - select { - case req := <-asSendCh: - if tc.UplinkMessage.Payload.GetMACPayload().Ack { - a.So(req.up.GetDownlinkAck(), should.HaveEmptyDiff, pb.MACState.PendingApplicationDownlink) - } else { - a.So(req.up.GetDownlinkNack(), should.HaveEmptyDiff, pb.MACState.PendingApplicationDownlink) - } - close(req.errch) + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtMergeMetadata(reqCtx, makeOTAAIdentifiers(nil), len(mds))) + }), should.BeTrue) { + return false + } + + var recentUp *ttnpb.UplinkMessage + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.SetByID to be called") + return false + + case req := <-env.DeviceRegistry.SetByID: + a.So(req.Context, should.HaveParentContextOrEqual, reqCtx) + a.So(req.ApplicationIdentifiers, should.Resemble, appID) + a.So(req.DeviceID, should.Resemble, devID) + a.So(req.Paths, should.HaveSameElementsDeep, joinSetByEUIGetPaths[:]) + dev, sets, err := req.Func(&ttnpb.EndDevice{ + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeDataUplink(33, true), + }, + QueuedApplicationDownlinks: []*ttnpb.ApplicationDownlink{ + makeApplicationDownlink(), + }, + }) + if !a.So(err, should.BeNil) || !a.So(dev, should.NotBeNil) { + return false + } + a.So(sets, should.HaveSameElementsDeep, joinSetByEUISetPaths[:]) + + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1) + macState.DesiredParameters.Rx1Delay = ttnpb.RX_DELAY_3 + macState.CurrentParameters.Rx1Delay = macState.DesiredParameters.Rx1Delay + macState.CurrentParameters.Channels = macState.DesiredParameters.Channels + macState.RxWindowsAvailable = true + macState.QueuedJoinAccept = &ttnpb.MACState_JoinAccept{ + Keys: *makeSessionKeys(ttnpb.MAC_V1_1), + Payload: joinResp.RawPayload, + Request: *joinReq, + } + a.So(dev.PendingMACState, should.Resemble, macState) + a.So(dev.QueuedApplicationDownlinks, should.BeNil) + if a.So(dev.RecentUplinks, should.NotBeEmpty) { + recentUp = dev.RecentUplinks[len(dev.RecentUplinks)-1] + a.So([]time.Time{start, recentUp.ReceivedAt, time.Now()}, should.BeChronological) + a.So(recentUp.RxMetadata, should.HaveSameElementsDiff, mds) + expectedUp := makeJoinRequest(true) + expectedUp.CorrelationIDs = reqCorrelationIDs + expectedUp.DeviceChannelIndex = 2 + expectedUp.ReceivedAt = recentUp.ReceivedAt + expectedUp.RxMetadata = recentUp.RxMetadata + expectedUp.Settings.DataRateIndex = ttnpb.DATA_RATE_1 + a.So(dev.RecentUplinks, should.HaveEmptyDiff, append(CopyUplinkMessages(getDevice.RecentUplinks...), expectedUp)) + } + req.Response <- DeviceRegistrySetByIDResponse{ + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(nil), + PendingMACState: macState, + QueuedApplicationDownlinks: dev.QueuedApplicationDownlinks, + RecentUplinks: dev.RecentUplinks, + CreatedAt: start, + UpdatedAt: time.Now(), + }, + } + } - case weReq := <-collectionDoneCh: - close(weReq.Response) - err := <-errch - a.So(err, should.BeNil) - t.Fatalf("Downlink (n)ack not sent to AS: %v", errors.Stack(err)) + if !a.So(AssertDownlinkTaskAddRequest(ctx, env.DownlinkTasks.Add, func(ctx context.Context, ids ttnpb.EndDeviceIdentifiers, startAt time.Time, replace bool) bool { + return a.So(ctx, should.HaveParentContextOrEqual, reqCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(nil)) && + a.So(startAt, should.Resemble, recentUp.ReceivedAt.Add(5*time.Second-NSScheduleWindow())) && + a.So(replace, should.BeTrue) + }, + nil, + ), should.BeTrue) { + return false + } - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for (n)ack to be sent to AS") + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, func(decoded bool) *ttnpb.UplinkMessage { + msg := makeJoinRequest(decoded) + if !decoded { + return msg } + msg.DeviceChannelIndex = 2 + msg.Settings.DataRateIndex = ttnpb.DATA_RATE_1 + return msg + }, start, duplicateCount) + + if !assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeNil) + }) { + return false } - var asUpReq asSendReq - select { - case asUpReq = <-asSendCh: - a.So(md, should.HaveSameElementsDeep, asUpReq.up.GetUplinkMessage().RxMetadata) - a.So(asUpReq.up.CorrelationIDs, should.NotBeEmpty) - a.So(asUpReq.up, should.Resemble, &ttnpb.ApplicationUp{ - EndDeviceIdentifiers: pb.EndDeviceIdentifiers, - CorrelationIDs: asUpReq.up.CorrelationIDs, - ReceivedAt: asUpReq.up.ReceivedAt, - Up: &ttnpb.ApplicationUp_UplinkMessage{UplinkMessage: &ttnpb.ApplicationUplink{ - FCnt: tc.NextFCntUp, - FPort: tc.UplinkMessage.Payload.GetMACPayload().FPort, - FRMPayload: tc.UplinkMessage.Payload.GetMACPayload().FRMPayload, - RxMetadata: asUpReq.up.GetUplinkMessage().RxMetadata, - SessionKeyID: pb.Session.SessionKeys.SessionKeyID, - Settings: asUpReq.up.GetUplinkMessage().Settings, - }}, - }) - close(asUpReq.errch) - - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for uplink to be sent to AS") + if asRecvCh != nil { + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for NetworkServer.handleASUplink to be called") + return false + + case req := <-asRecvCh: + a.So(req.Uplink, should.Resemble, &ttnpb.ApplicationUp{ + CorrelationIDs: reqCorrelationIDs, + EndDeviceIdentifiers: *makeOTAAIdentifiers(&joinReq.DevAddr), + Up: &ttnpb.ApplicationUp_JoinAccept{JoinAccept: &ttnpb.ApplicationJoinAccept{ + AppSKey: makeSessionKeys(ttnpb.MAC_V1_1).AppSKey, + InvalidatedDownlinks: []*ttnpb.ApplicationDownlink{ + makeApplicationDownlink(), + }, + SessionKeyID: makeSessionKeys(ttnpb.MAC_V1_1).SessionKeyID, + }}, + }) + req.Response <- ttnpb.Empty + } } - close(asSendCh) + return true + }, + }, - if !t.Run("device update", func(t *testing.T) { - a := assertions.New(t) + { + Name: "Rejoin-request", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) - ret, err := devReg.GetByID(ctx, pb.EndDeviceIdentifiers.ApplicationIdentifiers, pb.EndDeviceIdentifiers.DeviceID, ttnpb.EndDeviceFieldPathsTopLevel) - if !a.So(err, should.BeNil) || - !a.So(ret, should.NotBeNil) { - t.FailNow() - } + start := time.Now() - if !a.So(ret.RecentUplinks, should.NotBeEmpty) { - t.FailNow() - } + msg := makeRejoinRequest(false) - pb.Session.LastFCntUp = tc.NextFCntUp - pb.PendingSession = nil - pb.CreatedAt = ret.CreatedAt - pb.UpdatedAt = ret.UpdatedAt - if pb.MACState == nil { - macState, err := NewMACState(pb, ns.FrequencyPlans, ttnpb.MACSettings{}) - if !a.So(err, should.BeNil) { - t.FailNow() - } - pb.MACState = macState - } - pb.MACState.RxWindowsAvailable = true - pb.MACState.PendingApplicationDownlink = nil + handleUplinkErrCh := handle(ctx, msg) - msg := CopyUplinkMessage(tc.UplinkMessage) - msg.RxMetadata = md - msg.Settings.DataRateIndex = ttnpb.DATA_RATE_3 - msg.DeviceChannelIndex = 2 + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, makeRejoinRequest, start, duplicateCount) - pb.RecentUplinks = append(pb.RecentUplinks, msg) - if len(pb.RecentUplinks) > RecentUplinkCount { - pb.RecentUplinks = pb.RecentUplinks[len(pb.RecentUplinks)-RecentUplinkCount:] - } + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.NotBeNil) + }) + }, + }, - retUp := ret.RecentUplinks[len(ret.RecentUplinks)-1] - pbUp := pb.RecentUplinks[len(pb.RecentUplinks)-1] + { + Name: "Data uplink/Matching device/No concurrent update/1.0.2/First transmission/No ADR/Set success/Downlink add success", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) - a.So(retUp.ReceivedAt, should.HappenBetween, start, time.Now()) - pbUp.ReceivedAt = retUp.ReceivedAt + start := time.Now() - a.So(retUp.CorrelationIDs, should.NotBeEmpty) - pbUp.CorrelationIDs = retUp.CorrelationIDs + msg := makeLegacyDataUplink(34, false) - a.So(retUp.RxMetadata, should.HaveSameElementsDiff, pbUp.RxMetadata) - pbUp.RxMetadata = retUp.RxMetadata + handleUplinkErrCh := handle(ctx, msg) - a.So(ret, should.HaveEmptyDiff, pb) - }) { - t.FailNow() + rangeDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, + MACState: MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_0_2), + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(31, true), + makeLegacyDataUplink(32, true), + }, + Session: makeSession(ttnpb.MAC_V1_0_2, devAddr, 32), + CreatedAt: start, + UpdatedAt: time.Now(), } + var upCtx context.Context + var upCorrelationIDs []string select { - case req := <-downlinkAddCh: - a.So(req.ctx, should.HaveParentContext, ctx) - a.So(req.devID, should.Resemble, pb.EndDeviceIdentifiers) - a.So(req.replace, should.BeTrue) - a.So([]time.Time{start, req.t, time.Now()}, should.BeChronological) - - case <-time.After(Timeout): - t.Fatal("Timeout waiting for Add to be called") + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.RangeByAddr to be called") + return false + + case req := <-env.DeviceRegistry.RangeByAddr: + upCtx = req.Context + upCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(upCorrelationIDs, should.Contain, id) + } + a.So(upCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.DevAddr, should.Resemble, devAddr) + a.So(req.Paths, should.HaveSameElementsDeep, dataGetPaths[:]) + a.So(req.Func(CopyEndDevice(rangeDevice)), should.BeTrue) + multicastDevice := CopyEndDevice(rangeDevice) + multicastDevice.EndDeviceIdentifiers.DeviceID += "-multicast" + multicastDevice.Multicast = true + a.So(req.Func(multicastDevice), should.BeTrue) + fCntTooHighDevice := CopyEndDevice(rangeDevice) + fCntTooHighDevice.EndDeviceIdentifiers.DeviceID += "-too-high" + fCntTooHighDevice.RecentUplinks = append(fCntTooHighDevice.RecentUplinks, makeLegacyDataUplink(42, true)) + fCntTooHighDevice.Session.LastFCntUp = 42 + a.So(req.Func(fCntTooHighDevice), should.BeTrue) + req.Response <- nil } - _ = sendUplinkDuplicates(t, ns, collectionDoneCh, ctx, tc.UplinkMessage, DuplicateCount) - close(collectionDoneCh) + mds := sendUplinkDuplicates(ctx, handle, env.DeduplicationDone, bindMakeLegacyDataUplinkFCnt(34), start, duplicateCount) + mds = append(mds, msg.RxMetadata...) + var recentUp *ttnpb.UplinkMessage select { - case err := <-errch: - a.So(err, should.BeNil) + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.SetByID to be called") + return false + + case req := <-env.DeviceRegistry.SetByID: + a.So(req.Context, should.HaveParentContextOrEqual, upCtx) + a.So(req.ApplicationIdentifiers, should.Resemble, appID) + a.So(req.DeviceID, should.Resemble, devID) + a.So(req.Paths, should.HaveSameElementsDeep, dataGetPaths[:]) + dev, sets, err := req.Func(CopyEndDevice(rangeDevice)) + if !a.So(err, should.BeNil) || !a.So(dev, should.NotBeNil) { + return false + } + a.So(sets, should.HaveSameElementsDeep, []string{ + "mac_state", + "pending_mac_state", + "pending_session", + "recent_adr_uplinks", + "recent_uplinks", + "session", + }) - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for HandleUplink to return") + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_0_2) + macState.RxWindowsAvailable = true + macState.QueuedResponses = []*ttnpb.MACCommand{ + MakeLinkCheckAns(mds...), + } + a.So(dev.MACState, should.Resemble, macState) + a.So(dev.PendingMACState, should.BeNil) + a.So(dev.PendingSession, should.BeNil) + a.So(dev.RecentADRUplinks, should.BeNil) + a.So(dev.Session, should.Resemble, makeSession(ttnpb.MAC_V1_0_2, devAddr, 34)) + if a.So(dev.RecentUplinks, should.NotBeEmpty) { + recentUp = dev.RecentUplinks[len(dev.RecentUplinks)-1] + a.So([]time.Time{start, recentUp.ReceivedAt, time.Now()}, should.BeChronological) + a.So(recentUp.RxMetadata, should.HaveSameElementsDiff, mds) + expectedUp := makeLegacyDataUplink(34, true) + expectedUp.CorrelationIDs = upCorrelationIDs + expectedUp.DeviceChannelIndex = 1 + expectedUp.ReceivedAt = recentUp.ReceivedAt + expectedUp.RxMetadata = recentUp.RxMetadata + expectedUp.Settings.DataRateIndex = ttnpb.DATA_RATE_2 + a.So(dev.RecentUplinks, should.HaveEmptyDiff, append(CopyUplinkMessages(rangeDevice.RecentUplinks...), expectedUp)) + } + req.Response <- DeviceRegistrySetByIDResponse{ + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, + MACState: macState, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(31, true), + makeLegacyDataUplink(32, true), + makeLegacyDataUplink(34, true), + }, + Session: makeSession(ttnpb.MAC_V1_0_2, devAddr, 34), + CreatedAt: start, + UpdatedAt: time.Now(), + }, + } } - }) - } - } -} -func handleJoinTest() func(t *testing.T) { - return func(t *testing.T) { - authorizedCtx := clusterauth.NewContext(test.Context(), nil) - - t.Run("No device", func(t *testing.T) { - a := assertions.New(t) - - redisClient, flush := test.NewRedis(t, "networkserver_test") - defer flush() - defer redisClient.Close() - devReg := &redis.DeviceRegistry{Redis: redisClient} - - ns := test.Must(New( - component.MustNew(test.GetLogger(t), &component.Config{}), - &Config{ - Devices: devReg, - DeduplicationWindow: 42, - CooldownWindow: 42, - DownlinkTasks: &MockDownlinkTaskQueue{ - PopFunc: DownlinkTaskPopBlockFunc, - }, - })).(*NetworkServer) - ns.FrequencyPlans = frequencyplans.NewStore(test.FrequencyPlansFetcher) - test.Must(nil, ns.Start()) - defer ns.Close() + if !a.So(AssertDownlinkTaskAddRequest(ctx, env.DownlinkTasks.Add, func(ctx context.Context, ids ttnpb.EndDeviceIdentifiers, startAt time.Time, replace bool) bool { + return a.So(ctx, should.HaveParentContextOrEqual, upCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(&devAddr)) && + a.So(startAt, should.Resemble, recentUp.ReceivedAt.Add(time.Second-NSScheduleWindow())) && + a.So(replace, should.BeTrue) + }, + nil, + ), should.BeTrue) { + return false + } - _, err := ns.HandleUplink(authorizedCtx, ttnpb.NewPopulatedUplinkMessageJoinRequest(test.Randy)) - a.So(err, should.NotBeNil) - }) + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtMergeMetadata(upCtx, rangeDevice.EndDeviceIdentifiers, len(mds))) + }), should.BeTrue) { + return false + } - for _, tc := range []struct { - Name string - - Device *ttnpb.EndDevice - UplinkMessage *ttnpb.UplinkMessage - }{ - { - "1.1/no pending session", - &ttnpb.EndDevice{ - LoRaWANVersion: ttnpb.MAC_V1_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - DevEUI: &DevEUI, - JoinEUI: &JoinEUI, - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ApplicationID: ApplicationID}, - DeviceID: DeviceID, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - SupportsJoin: true, - }, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageJoinRequest(test.Randy) - msg.Settings.Frequency = 868500000 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtReceiveLinkCheckRequest(upCtx, rangeDevice.EndDeviceIdentifiers, nil)) + }), should.BeTrue) { + return false + } - jr := msg.Payload.GetJoinRequestPayload() - jr.DevEUI = DevEUI - jr.JoinEUI = JoinEUI + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtEnqueueLinkCheckAnswer(upCtx, rangeDevice.EndDeviceIdentifiers, MakeLinkCheckAns(mds...).GetLinkCheckAns())) + }), should.BeTrue) { + return false + } - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtForwardDataUplink(upCtx, rangeDevice.EndDeviceIdentifiers, nil)) + }), should.BeTrue) { + return false + } + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, func(decoded bool) *ttnpb.UplinkMessage { + msg := makeLegacyDataUplink(34, decoded) + if !decoded { + return msg + } + msg.DeviceChannelIndex = 1 + msg.Settings.DataRateIndex = ttnpb.DATA_RATE_2 return msg - }(), - }, - { - "1.1/pending session", - &ttnpb.EndDevice{ - LoRaWANVersion: ttnpb.MAC_V1_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - DevEUI: &DevEUI, - JoinEUI: &JoinEUI, - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ApplicationID: ApplicationID}, - DeviceID: DeviceID, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - PendingSession: ttnpb.NewPopulatedSession(test.Randy, false), - PendingMACState: ttnpb.NewPopulatedMACState(test.Randy, false), - SupportsJoin: true, - }, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageJoinRequest(test.Randy) - msg.Settings.Frequency = 868500000 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate + }, start, duplicateCount) - jr := msg.Payload.GetJoinRequestPayload() - jr.DevEUI = DevEUI - jr.JoinEUI = JoinEUI - - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + if !assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeNil) + }) { + return false + } - return msg - }(), + if asRecvCh != nil { + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for NetworkServer.handleASUplink to be called") + return false + + case req := <-asRecvCh: + a.So(req.Uplink, should.Resemble, &ttnpb.ApplicationUp{ + CorrelationIDs: upCorrelationIDs, + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + Up: &ttnpb.ApplicationUp_UplinkMessage{UplinkMessage: &ttnpb.ApplicationUplink{ + SessionKeyID: makeSessionKeys(ttnpb.MAC_V1_1).SessionKeyID, + FPort: fPort, + FCnt: 34, + FRMPayload: makeDataUplinkFRMPayload(34), + RxMetadata: recentUp.RxMetadata, + Settings: ttnpb.TxSettings{ + DataRateIndex: ttnpb.DATA_RATE_2, + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 10, + }}, + }, + EnableCRC: true, + Frequency: 868300000, + Timestamp: 42, + }, + }}, + }) + req.Response <- ttnpb.Empty + } + } + return true }, - { - "1.0.2", - &ttnpb.EndDevice{ - LoRaWANVersion: ttnpb.MAC_V1_0_2, - LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - DevEUI: &DevEUI, - JoinEUI: &JoinEUI, - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ApplicationID: ApplicationID}, - DeviceID: DeviceID, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - PendingSession: ttnpb.NewPopulatedSession(test.Randy, false), - PendingMACState: ttnpb.NewPopulatedMACState(test.Randy, false), - SupportsJoin: true, - }, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageJoinRequest(test.Randy) - msg.Settings.Frequency = 868500000 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate - - jr := msg.Payload.GetJoinRequestPayload() - jr.DevEUI = DevEUI - jr.JoinEUI = JoinEUI + }, - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + { + Name: "Data uplink/Matching device/No concurrent update/1.1/First transmission/No ADR/Set success/Downlink add success", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) - return msg - }(), - }, - { - "1.0.1", - &ttnpb.EndDevice{ - LoRaWANVersion: ttnpb.MAC_V1_0_1, - LoRaWANPHYVersion: ttnpb.PHY_V1_0_1, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - DevEUI: &DevEUI, - JoinEUI: &JoinEUI, - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ApplicationID: ApplicationID}, - DeviceID: DeviceID, - }, - FrequencyPlanID: test.EUFrequencyPlanID, - PendingSession: ttnpb.NewPopulatedSession(test.Randy, false), - PendingMACState: ttnpb.NewPopulatedMACState(test.Randy, false), - SupportsJoin: true, - }, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageJoinRequest(test.Randy) - msg.Settings.Frequency = 868500000 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate + start := time.Now() - jr := msg.Payload.GetJoinRequestPayload() - jr.DevEUI = DevEUI - jr.JoinEUI = JoinEUI + msg := makeDataUplink(34, false) - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + handleUplinkErrCh := handle(ctx, msg) - return msg - }(), - }, - { - "1.0", - &ttnpb.EndDevice{ - LoRaWANVersion: ttnpb.MAC_V1_0, - LoRaWANPHYVersion: ttnpb.PHY_V1_0, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - DevEUI: &DevEUI, - JoinEUI: &JoinEUI, - ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ApplicationID: ApplicationID}, - DeviceID: DeviceID, + rangeDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1), + RecentUplinks: []*ttnpb.UplinkMessage{ + makeDataUplink(31, true), + makeDataUplink(32, true), }, - FrequencyPlanID: test.EUFrequencyPlanID, - PendingSession: ttnpb.NewPopulatedSession(test.Randy, false), - PendingMACState: ttnpb.NewPopulatedMACState(test.Randy, false), - SupportsJoin: true, - }, - func() *ttnpb.UplinkMessage { - msg := ttnpb.NewPopulatedUplinkMessageJoinRequest(test.Randy) - msg.Settings.Frequency = 868500000 - msg.Settings.DataRate = band.All[band.EU_863_870].DataRates[3].Rate - - jr := msg.Payload.GetJoinRequestPayload() - jr.DevEUI = DevEUI - jr.JoinEUI = JoinEUI + Session: makeSession(ttnpb.MAC_V1_1, devAddr, 32), + CreatedAt: start, + UpdatedAt: time.Now(), + } - msg.RawPayload = test.Must(lorawan.MarshalMessage(*msg.Payload)).([]byte) + var upCtx context.Context + var upCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.RangeByAddr to be called") + return false + + case req := <-env.DeviceRegistry.RangeByAddr: + upCtx = req.Context + upCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(upCorrelationIDs, should.Contain, id) + } + a.So(upCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.DevAddr, should.Resemble, devAddr) + a.So(req.Paths, should.HaveSameElementsDeep, dataGetPaths[:]) + a.So(req.Func(CopyEndDevice(rangeDevice)), should.BeTrue) + multicastDevice := CopyEndDevice(rangeDevice) + multicastDevice.EndDeviceIdentifiers.DeviceID += "-multicast" + multicastDevice.Multicast = true + a.So(req.Func(multicastDevice), should.BeTrue) + fCntTooHighDevice := CopyEndDevice(rangeDevice) + fCntTooHighDevice.EndDeviceIdentifiers.DeviceID += "-too-high" + fCntTooHighDevice.RecentUplinks = append(fCntTooHighDevice.RecentUplinks, makeLegacyDataUplink(42, true)) + fCntTooHighDevice.Session.LastFCntUp = 42 + a.So(req.Func(fCntTooHighDevice), should.BeTrue) + req.Response <- nil + } - return msg - }(), - }, - } { - t.Run(tc.Name, func(t *testing.T) { - a := assertions.New(t) + mds := sendUplinkDuplicates(ctx, handle, env.DeduplicationDone, bindMakeDataUplinkFCnt(34), start, duplicateCount) + mds = append(mds, msg.RxMetadata...) - authorizedCtx := test.ContextWithT(authorizedCtx, t) - - redisClient, flush := test.NewRedis(t, "networkserver_test") - defer flush() - defer redisClient.Close() - devReg := &redis.DeviceRegistry{Redis: redisClient} - - // Fill DeviceRegistry with devices - for i := 0; i < DeviceCount; i++ { - pb := &ttnpb.EndDevice{} - if err := pb.SetFields(ttnpb.NewPopulatedEndDevice(test.Randy, false), []string{ - "frequency_plan_id", - "ids", - "lorawan_phy_version", - "lorawan_version", + var recentUp *ttnpb.UplinkMessage + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.SetByID to be called") + return false + + case req := <-env.DeviceRegistry.SetByID: + a.So(req.Context, should.HaveParentContextOrEqual, upCtx) + a.So(req.ApplicationIdentifiers, should.Resemble, appID) + a.So(req.DeviceID, should.Resemble, devID) + a.So(req.Paths, should.HaveSameElementsDeep, dataGetPaths[:]) + dev, sets, err := req.Func(CopyEndDevice(rangeDevice)) + if !a.So(err, should.BeNil) || !a.So(dev, should.NotBeNil) { + return false + } + a.So(sets, should.HaveSameElementsDeep, []string{ "mac_state", + "pending_mac_state", "pending_session", + "recent_adr_uplinks", + "recent_uplinks", "session", - "supports_join", - }...); err != nil { - t.Fatalf("Failed to set fields: %s", err) - } - - ret, err := devReg.SetByID(authorizedCtx, pb.ApplicationIdentifiers, pb.DeviceID, - []string{ - "created_at", - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "mac_state", - "pending_session", - "session", - "supports_join", - "updated_at", + }) + + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_1) + macState.RxWindowsAvailable = true + macState.QueuedResponses = []*ttnpb.MACCommand{ + MakeLinkCheckAns(mds...), + } + a.So(dev.MACState, should.Resemble, macState) + a.So(dev.PendingMACState, should.BeNil) + a.So(dev.PendingSession, should.BeNil) + a.So(dev.RecentADRUplinks, should.BeNil) + a.So(dev.Session, should.Resemble, makeSession(ttnpb.MAC_V1_1, devAddr, 34)) + if a.So(dev.RecentUplinks, should.NotBeEmpty) { + recentUp = dev.RecentUplinks[len(dev.RecentUplinks)-1] + a.So([]time.Time{start, recentUp.ReceivedAt, time.Now()}, should.BeChronological) + a.So(recentUp.RxMetadata, should.HaveSameElementsDiff, mds) + expectedUp := makeDataUplink(34, true) + expectedUp.CorrelationIDs = upCorrelationIDs + expectedUp.DeviceChannelIndex = 1 + expectedUp.ReceivedAt = recentUp.ReceivedAt + expectedUp.RxMetadata = recentUp.RxMetadata + expectedUp.Settings.DataRateIndex = ttnpb.DATA_RATE_2 + a.So(dev.RecentUplinks, should.HaveEmptyDiff, append(CopyUplinkMessages(rangeDevice.RecentUplinks...), expectedUp)) + } + req.Response <- DeviceRegistrySetByIDResponse{ + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_1_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_1, + MACState: macState, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeDataUplink(31, true), + makeDataUplink(32, true), + makeDataUplink(34, true), + }, + Session: makeSession(ttnpb.MAC_V1_1, devAddr, 34), + CreatedAt: start, + UpdatedAt: time.Now(), }, - func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { - if !a.So(stored, should.BeNil) { - t.Fatal("Registry is not empty") - } - return CopyEndDevice(pb), []string{ - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "mac_state", - "pending_session", - "session", - "supports_join", - }, nil - }) - if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) { - t.Fatalf("Failed to create device, error: %s", err) } - pb.CreatedAt = ret.CreatedAt - pb.UpdatedAt = ret.UpdatedAt - a.So(ret, should.Resemble, pb) } - type handleJoinRequest struct { - ctx context.Context - req *ttnpb.JoinRequest - ch chan<- *ttnpb.JoinResponse - errch chan<- error + if !a.So(AssertDownlinkTaskAddRequest(ctx, env.DownlinkTasks.Add, func(ctx context.Context, ids ttnpb.EndDeviceIdentifiers, startAt time.Time, replace bool) bool { + return a.So(ctx, should.HaveParentContextOrEqual, upCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(&devAddr)) && + a.So(startAt, should.Resemble, recentUp.ReceivedAt.Add(time.Second-NSScheduleWindow())) && + a.So(replace, should.BeTrue) + }, + nil, + ), should.BeTrue) { + return false } - type downlinkTasksAddRequest struct { - ctx context.Context - devID ttnpb.EndDeviceIdentifiers - t time.Time - replace bool + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtMergeMetadata(upCtx, rangeDevice.EndDeviceIdentifiers, len(mds))) + }), should.BeTrue) { + return false } - downlinkAddCh := make(chan downlinkTasksAddRequest, 1) - deduplicationDoneCh := make(chan WindowEndRequest, 1) - defer func() { - close(deduplicationDoneCh) - }() + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtReceiveLinkCheckRequest(upCtx, rangeDevice.EndDeviceIdentifiers, nil)) + }), should.BeTrue) { + return false + } - collectionDoneCh := make(chan WindowEndRequest, 1) - defer func() { - close(collectionDoneCh) - }() + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtEnqueueLinkCheckAnswer(upCtx, rangeDevice.EndDeviceIdentifiers, MakeLinkCheckAns(mds...).GetLinkCheckAns())) + }), should.BeTrue) { + return false + } - handleJoinCh := make(chan handleJoinRequest, 1) - defer func() { - close(handleJoinCh) - }() + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtForwardDataUplink(upCtx, rangeDevice.EndDeviceIdentifiers, nil)) + }), should.BeTrue) { + return false + } - asSendCh := make(chan *ttnpb.ApplicationUp) - defer func() { - close(asSendCh) - }() + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, func(decoded bool) *ttnpb.UplinkMessage { + msg := makeDataUplink(34, decoded) + if !decoded { + return msg + } + msg.DeviceChannelIndex = 1 + msg.Settings.DataRateIndex = ttnpb.DATA_RATE_2 + return msg + }, start, duplicateCount) - keys := ttnpb.NewPopulatedSessionKeys(test.Randy, false) - if tc.Device.LoRaWANVersion.Compare(ttnpb.MAC_V1_1) < 0 { - keys.NwkSEncKey = keys.FNwkSIntKey - keys.SNwkSIntKey = keys.FNwkSIntKey - } - - ns := test.Must(New( - component.MustNew(test.GetLogger(t), &component.Config{}), - &Config{ - NetID: NetID, - Devices: devReg, - DownlinkTasks: &MockDownlinkTaskQueue{ - PopFunc: DownlinkTaskPopBlockFunc, - AddFunc: func(ctx context.Context, devID ttnpb.EndDeviceIdentifiers, t time.Time, replace bool) error { - downlinkAddCh <- downlinkTasksAddRequest{ - ctx: ctx, - devID: devID, - t: t, - replace: replace, - } - return nil - }, - }, - }, - WithCollectionDoneFunc(MakeWindowEndChFunc(collectionDoneCh)), - WithDeduplicationDoneFunc(MakeWindowEndChFunc(deduplicationDoneCh)), - WithNsJsClientFunc(func(ctx context.Context, id ttnpb.EndDeviceIdentifiers) (ttnpb.NsJsClient, error) { - return &MockNsJsClient{ - GetNwkSKeysFunc: func(ctx context.Context, req *ttnpb.SessionKeyRequest, _ ...grpc.CallOption) (*ttnpb.NwkSKeysResponse, error) { - err := errors.New("GetNwkSKeys should not be called") - test.MustTFromContext(ctx).Error(err) - return nil, err - }, - HandleJoinFunc: func(ctx context.Context, req *ttnpb.JoinRequest, _ ...grpc.CallOption) (*ttnpb.JoinResponse, error) { - ch := make(chan *ttnpb.JoinResponse, 1) - errch := make(chan error, 1) - handleJoinCh <- handleJoinRequest{ctx, req, ch, errch} - return <-ch, <-errch - }, - }, nil - }), - WithASUplinkHandler(func(ctx context.Context, ids ttnpb.ApplicationIdentifiers, up *ttnpb.ApplicationUp) (bool, error) { - asSendCh <- up - return true, nil - }), - )).(*NetworkServer) - ns.FrequencyPlans = frequencyplans.NewStore(test.FrequencyPlansFetcher) + if !assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeNil) + }) { + return false + } - test.Must(nil, ns.Start()) - defer ns.Close() + if asRecvCh != nil { + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for NetworkServer.handleASUplink to be called") + return false + + case req := <-asRecvCh: + a.So(req.Uplink, should.Resemble, &ttnpb.ApplicationUp{ + CorrelationIDs: upCorrelationIDs, + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + Up: &ttnpb.ApplicationUp_UplinkMessage{UplinkMessage: &ttnpb.ApplicationUplink{ + SessionKeyID: makeSessionKeys(ttnpb.MAC_V1_1).SessionKeyID, + FPort: fPort, + FCnt: 34, + FRMPayload: makeDataUplinkFRMPayload(34), + RxMetadata: recentUp.RxMetadata, + Settings: ttnpb.TxSettings{ + DataRateIndex: ttnpb.DATA_RATE_2, + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 10, + }}, + }, + EnableCRC: true, + Frequency: 868300000, + Timestamp: 42, + }, + }}, + }) + req.Response <- ttnpb.Empty + } + } + return true + }, + }, - pb := CopyEndDevice(tc.Device) + { + Name: "Data uplink/Matching device/Concurrent update/1.0.2/First transmission/No ADR/Set success/Downlink add success", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) start := time.Now() - ret, err := devReg.SetByID(authorizedCtx, pb.ApplicationIdentifiers, pb.DeviceID, - []string{ - "created_at", - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "mac_settings", + msg := makeLegacyDataUplink(34, false) + + handleUplinkErrCh := handle(ctx, msg) + + rangeDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, + MACState: MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_0_2), + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(31, true), + makeLegacyDataUplink(32, true), + }, + Session: makeSession(ttnpb.MAC_V1_0_2, devAddr, 32), + CreatedAt: start, + UpdatedAt: time.Now(), + } + + var upCtx context.Context + var upCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.RangeByAddr to be called") + return false + + case req := <-env.DeviceRegistry.RangeByAddr: + upCtx = req.Context + upCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(upCorrelationIDs, should.Contain, id) + } + a.So(upCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.DevAddr, should.Resemble, devAddr) + a.So(req.Paths, should.HaveSameElementsDeep, dataGetPaths[:]) + a.So(req.Func(CopyEndDevice(rangeDevice)), should.BeTrue) + multicastDevice := CopyEndDevice(rangeDevice) + multicastDevice.EndDeviceIdentifiers.DeviceID += "-multicast" + multicastDevice.Multicast = true + a.So(req.Func(multicastDevice), should.BeTrue) + fCntTooHighDevice := CopyEndDevice(rangeDevice) + fCntTooHighDevice.EndDeviceIdentifiers.DeviceID += "-too-high" + fCntTooHighDevice.RecentUplinks = append(fCntTooHighDevice.RecentUplinks, makeLegacyDataUplink(42, true)) + fCntTooHighDevice.Session.LastFCntUp = 42 + a.So(req.Func(fCntTooHighDevice), should.BeTrue) + req.Response <- nil + } + + mds := sendUplinkDuplicates(ctx, handle, env.DeduplicationDone, bindMakeLegacyDataUplinkFCnt(34), start, duplicateCount) + mds = append(mds, msg.RxMetadata...) + + var recentUp *ttnpb.UplinkMessage + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.SetByID to be called") + return false + + case req := <-env.DeviceRegistry.SetByID: + a.So(req.Context, should.HaveParentContextOrEqual, upCtx) + a.So(req.ApplicationIdentifiers, should.Resemble, appID) + a.So(req.DeviceID, should.Resemble, devID) + a.So(req.Paths, should.HaveSameElementsDeep, dataGetPaths[:]) + updatedDevice := CopyEndDevice(rangeDevice) + updatedDevice.UpdatedAt = time.Now() + dev, sets, err := req.Func(updatedDevice) + if !a.So(err, should.BeNil) || !a.So(dev, should.NotBeNil) { + return false + } + a.So(sets, should.HaveSameElementsDeep, []string{ "mac_state", "pending_mac_state", "pending_session", - "recent_downlinks", + "recent_adr_uplinks", + "recent_uplinks", "session", - "updated_at", - "supports_join", - }, - func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { - if !a.So(stored, should.BeNil) { - t.Fatal("Registry is not empty") - } - return CopyEndDevice(tc.Device), []string{ - "frequency_plan_id", - "ids.application_ids", - "ids.dev_addr", - "ids.dev_eui", - "ids.device_id", - "ids.join_eui", - "lorawan_phy_version", - "lorawan_version", - "mac_settings", - "mac_state", - "pending_mac_state", - "pending_session", - "recent_downlinks", - "session", - "supports_join", - }, nil }) - if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) { - t.Fatalf("Failed to create device, error: %s", err) - } - pb.CreatedAt = ret.CreatedAt - a.So(ret.UpdatedAt, should.HappenAfter, start) - pb.UpdatedAt = ret.UpdatedAt - a.So(ret, should.HaveEmptyDiff, pb) - - macState, err := NewMACState(ret, ns.FrequencyPlans, ttnpb.MACSettings{}) - if !a.So(err, should.BeNil) { - t.Fatalf("Failed to reset MAC state: %s", err) - } - ret.PendingMACState = macState - pb = ret - - expectedRequest := &ttnpb.JoinRequest{ - RawPayload: append(tc.UplinkMessage.RawPayload[:0:0], tc.UplinkMessage.RawPayload...), - NetID: NetID, - SelectedMACVersion: pb.LoRaWANVersion, - RxDelay: pb.PendingMACState.DesiredParameters.Rx1Delay, - CFList: frequencyplans.CFList(*test.Must(ns.FrequencyPlans.GetByID(test.EUFrequencyPlanID)).(*frequencyplans.FrequencyPlan), pb.LoRaWANPHYVersion), - DownlinkSettings: ttnpb.DLSettings{ - Rx1DROffset: pb.PendingMACState.DesiredParameters.Rx1DataRateOffset, - Rx2DR: pb.PendingMACState.DesiredParameters.Rx2DataRateIndex, - OptNeg: pb.LoRaWANVersion.Compare(ttnpb.MAC_V1_1) >= 0, - }, - } - if expectedRequest.CFList != nil { - switch expectedRequest.CFList.Type { - case ttnpb.CFListType_FREQUENCIES: - phy := test.Must(test.Must(band.GetByID(pb.FrequencyPlanID)).(band.Band).Version(pb.LoRaWANPHYVersion)).(band.Band) - for _, freq := range expectedRequest.CFList.Freq { - if freq == 0 { - break - } - pb.PendingMACState.CurrentParameters.Channels = append(pb.PendingMACState.CurrentParameters.Channels, &ttnpb.MACParameters_Channel{ - UplinkFrequency: uint64(freq * 100), - DownlinkFrequency: uint64(freq * 100), - MaxDataRateIndex: ttnpb.DataRateIndex(phy.MaxADRDataRateIndex), - EnableUplink: true, - }) - } - case ttnpb.CFListType_CHANNEL_MASKS: - if len(pb.PendingMACState.CurrentParameters.Channels) != len(expectedRequest.CFList.ChMasks) { - t.Fatal("Mismatch in CFList mask length and pending MAC state channel count") - } - for i, m := range expectedRequest.CFList.ChMasks { - pb.PendingMACState.CurrentParameters.Channels[i].EnableUplink = m - } + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_0_2) + macState.RxWindowsAvailable = true + macState.QueuedResponses = []*ttnpb.MACCommand{ + MakeLinkCheckAns(mds...), + } + a.So(dev.MACState, should.Resemble, macState) + a.So(dev.PendingMACState, should.BeNil) + a.So(dev.PendingSession, should.BeNil) + a.So(dev.RecentADRUplinks, should.BeNil) + a.So(dev.Session, should.Resemble, makeSession(ttnpb.MAC_V1_0_2, devAddr, 34)) + if a.So(dev.RecentUplinks, should.NotBeEmpty) { + recentUp = dev.RecentUplinks[len(dev.RecentUplinks)-1] + a.So([]time.Time{start, recentUp.ReceivedAt, time.Now()}, should.BeChronological) + a.So(recentUp.RxMetadata, should.HaveSameElementsDiff, mds) + expectedUp := makeLegacyDataUplink(34, true) + expectedUp.CorrelationIDs = upCorrelationIDs + expectedUp.DeviceChannelIndex = 1 + expectedUp.ReceivedAt = recentUp.ReceivedAt + expectedUp.RxMetadata = recentUp.RxMetadata + expectedUp.Settings.DataRateIndex = ttnpb.DATA_RATE_2 + a.So(dev.RecentUplinks, should.HaveEmptyDiff, append(CopyUplinkMessages(rangeDevice.RecentUplinks...), expectedUp)) + } + req.Response <- DeviceRegistrySetByIDResponse{ + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, + MACState: macState, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(31, true), + makeLegacyDataUplink(32, true), + makeLegacyDataUplink(34, true), + }, + Session: makeSession(ttnpb.MAC_V1_0_2, devAddr, 34), + CreatedAt: start, + UpdatedAt: time.Now(), + }, } } - ctx := context.WithValue(authorizedCtx, struct{}{}, 42) - ctx = log.NewContext(ctx, test.GetLogger(t)) - - resp := &ttnpb.JoinResponse{ - RawPayload: bytes.Repeat([]byte{42}, 17), - SessionKeys: *keys, + if !a.So(AssertDownlinkTaskAddRequest(ctx, env.DownlinkTasks.Add, func(ctx context.Context, ids ttnpb.EndDeviceIdentifiers, startAt time.Time, replace bool) bool { + return a.So(ctx, should.HaveParentContextOrEqual, upCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(&devAddr)) && + a.So(startAt, should.Resemble, recentUp.ReceivedAt.Add(time.Second-NSScheduleWindow())) && + a.So(replace, should.BeTrue) + }, + nil, + ), should.BeTrue) { + return false } - errch := make(chan error, 1) - go func() { - _, err := ns.HandleUplink(ctx, CopyUplinkMessage(tc.UplinkMessage)) - errch <- err - }() + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtMergeMetadata(upCtx, rangeDevice.EndDeviceIdentifiers, len(mds))) + }), should.BeTrue) { + return false + } - select { - case req := <-handleJoinCh: - if ses := tc.Device.Session; ses != nil { - a.So(req.req.DevAddr, should.NotResemble, ses.DevAddr) - } - a.So(req.req.CorrelationIDs, should.HaveLength, 1) + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtReceiveLinkCheckRequest(upCtx, rangeDevice.EndDeviceIdentifiers, nil)) + }), should.BeTrue) { + return false + } - expectedRequest.DevAddr = req.req.DevAddr - expectedRequest.CorrelationIDs = req.req.CorrelationIDs - a.So(req.req, should.Resemble, expectedRequest) + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtEnqueueLinkCheckAnswer(upCtx, rangeDevice.EndDeviceIdentifiers, MakeLinkCheckAns(mds...).GetLinkCheckAns())) + }), should.BeTrue) { + return false + } - req.ch <- resp - req.errch <- nil + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtForwardDataUplink(upCtx, rangeDevice.EndDeviceIdentifiers, nil)) + }), should.BeTrue) { + return false + } - case weReq := <-collectionDoneCh: - close(weReq.Response) - a.So(<-errch, should.BeNil) - t.Fatal("Join-request not sent to JS") + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, func(decoded bool) *ttnpb.UplinkMessage { + msg := makeLegacyDataUplink(34, decoded) + if !decoded { + return msg + } + msg.DeviceChannelIndex = 1 + msg.Settings.DataRateIndex = ttnpb.DATA_RATE_2 + return msg + }, start, duplicateCount) - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for join-request to be sent to JS") + if !assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeNil) + }) { + return false } - md := sendUplinkDuplicates(t, ns, deduplicationDoneCh, ctx, tc.UplinkMessage, DuplicateCount) + if asRecvCh != nil { + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for NetworkServer.handleASUplink to be called") + return false + + case req := <-asRecvCh: + a.So(req.Uplink, should.Resemble, &ttnpb.ApplicationUp{ + CorrelationIDs: upCorrelationIDs, + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + Up: &ttnpb.ApplicationUp_UplinkMessage{UplinkMessage: &ttnpb.ApplicationUplink{ + SessionKeyID: makeSessionKeys(ttnpb.MAC_V1_1).SessionKeyID, + FPort: fPort, + FCnt: 34, + FRMPayload: makeDataUplinkFRMPayload(34), + RxMetadata: recentUp.RxMetadata, + Settings: ttnpb.TxSettings{ + DataRateIndex: ttnpb.DATA_RATE_2, + DataRate: ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_LoRa{LoRa: &ttnpb.LoRaDataRate{ + Bandwidth: 125000, + SpreadingFactor: 10, + }}, + }, + EnableCRC: true, + Frequency: 868300000, + Timestamp: 42, + }, + }}, + }) + req.Response <- ttnpb.Empty + } + } + return true + }, + }, - select { - case up := <-asSendCh: - if !t.Run("device update", func(t *testing.T) { - a := assertions.New(t) - - ret, err := devReg.GetByID(authorizedCtx, pb.EndDeviceIdentifiers.ApplicationIdentifiers, pb.EndDeviceIdentifiers.DeviceID, ttnpb.EndDeviceFieldPathsTopLevel) - if !a.So(err, should.BeNil) || - !a.So(ret, should.NotBeNil) || - !a.So(ret.RecentUplinks, should.NotBeEmpty) { - t.FailNow() - } + { + Name: "Data uplink/Matching device/No concurrent update/1.0.2/Second transmission/No ADR/Set success/Downlink add success", + Handler: func(ctx context.Context, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, handle func(context.Context, *ttnpb.UplinkMessage) <-chan error) bool { + t := test.MustTFromContext(ctx) + a := assertions.New(t) - pb.PendingMACState.RxWindowsAvailable = true - pb.PendingMACState.QueuedJoinAccept = &ttnpb.MACState_JoinAccept{ - Payload: resp.RawPayload, - Request: *deepcopy.Copy(expectedRequest).(*ttnpb.JoinRequest), - Keys: *keys, - } - pb.CreatedAt = ret.CreatedAt - pb.UpdatedAt = ret.UpdatedAt - pb.QueuedApplicationDownlinks = nil - - msg := CopyUplinkMessage(tc.UplinkMessage) - msg.RxMetadata = md - msg.DeviceChannelIndex = 2 - msg.Settings.DataRateIndex = ttnpb.DATA_RATE_3 - - pb.RecentUplinks = append(pb.RecentUplinks, msg) - if len(pb.RecentUplinks) > RecentUplinkCount { - pb.RecentUplinks = pb.RecentUplinks[len(pb.RecentUplinks)-RecentUplinkCount:] - } + start := time.Now() - retUp := ret.RecentUplinks[len(ret.RecentUplinks)-1] - pbUp := pb.RecentUplinks[len(pb.RecentUplinks)-1] + msg := makeLegacyDataUplink(34, false) - a.So(retUp.ReceivedAt, should.HappenBetween, start, time.Now()) - pbUp.ReceivedAt = retUp.ReceivedAt + handleUplinkErrCh := handle(ctx, msg) - a.So(retUp.CorrelationIDs, should.NotBeEmpty) - pbUp.CorrelationIDs = retUp.CorrelationIDs + makeMACState := func() *ttnpb.MACState { + macState := MakeDefaultEU868MACState(ttnpb.CLASS_A, ttnpb.MAC_V1_0_2) + macState.CurrentParameters.ADRNbTrans = 2 + macState.DesiredParameters.ADRNbTrans = 2 + return macState + } - a.So(retUp.RxMetadata, should.HaveSameElementsDiff, pbUp.RxMetadata) - pbUp.RxMetadata = retUp.RxMetadata + rangeDevice := &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, + MACState: makeMACState(), + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(31, true), + makeLegacyDataUplink(32, true), + makeLegacyDataUplink(34, true), + }, + Session: makeSession(ttnpb.MAC_V1_0_2, devAddr, 34), + CreatedAt: start, + UpdatedAt: time.Now(), + } - a.So(ret, should.HaveEmptyDiff, pb) - }) { - t.FailNow() + var upCtx context.Context + var upCorrelationIDs []string + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.RangeByAddr to be called") + return false + + case req := <-env.DeviceRegistry.RangeByAddr: + upCtx = req.Context + upCorrelationIDs = events.CorrelationIDsFromContext(req.Context) + for _, id := range correlationIDs { + a.So(upCorrelationIDs, should.Contain, id) } + a.So(upCorrelationIDs, should.HaveLength, len(correlationIDs)+2) + a.So(req.DevAddr, should.Resemble, devAddr) + a.So(req.Paths, should.HaveSameElementsDeep, dataGetPaths[:]) + a.So(req.Func(CopyEndDevice(rangeDevice)), should.BeTrue) + multicastDevice := CopyEndDevice(rangeDevice) + multicastDevice.EndDeviceIdentifiers.DeviceID += "-multicast" + multicastDevice.Multicast = true + a.So(req.Func(multicastDevice), should.BeTrue) + fCntTooHighDevice := CopyEndDevice(rangeDevice) + fCntTooHighDevice.EndDeviceIdentifiers.DeviceID += "-too-high" + fCntTooHighDevice.RecentUplinks = append(fCntTooHighDevice.RecentUplinks, makeLegacyDataUplink(42, true)) + fCntTooHighDevice.Session.LastFCntUp = 42 + a.So(req.Func(fCntTooHighDevice), should.BeTrue) + req.Response <- nil + } - if !a.So(up, should.NotBeNil) { - t.Fatal(" AS uplink received") + mds := sendUplinkDuplicates(ctx, handle, env.DeduplicationDone, bindMakeLegacyDataUplinkFCnt(34), start, duplicateCount) + mds = append(mds, msg.RxMetadata...) + + var recentUp *ttnpb.UplinkMessage + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for DeviceRegistry.SetByID to be called") + return false + + case req := <-env.DeviceRegistry.SetByID: + a.So(req.Context, should.HaveParentContextOrEqual, upCtx) + a.So(req.ApplicationIdentifiers, should.Resemble, appID) + a.So(req.DeviceID, should.Resemble, devID) + a.So(req.Paths, should.HaveSameElementsDeep, dataGetPaths[:]) + dev, sets, err := req.Func(CopyEndDevice(rangeDevice)) + if !a.So(err, should.BeNil) || !a.So(dev, should.NotBeNil) { + return false } - a.So(up.CorrelationIDs, should.NotBeEmpty) - a.So(up, should.HaveEmptyDiff, &ttnpb.ApplicationUp{ - CorrelationIDs: up.CorrelationIDs, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - ApplicationIdentifiers: pb.EndDeviceIdentifiers.ApplicationIdentifiers, - DeviceID: pb.EndDeviceIdentifiers.DeviceID, - DevEUI: pb.EndDeviceIdentifiers.DevEUI, - JoinEUI: pb.EndDeviceIdentifiers.JoinEUI, - DevAddr: &expectedRequest.DevAddr, - }, - ReceivedAt: up.ReceivedAt, - Up: &ttnpb.ApplicationUp_JoinAccept{JoinAccept: &ttnpb.ApplicationJoinAccept{ - AppSKey: resp.SessionKeys.AppSKey, - SessionKeyID: resp.SessionKeys.SessionKeyID, - InvalidatedDownlinks: tc.Device.QueuedApplicationDownlinks, - }}, + a.So(sets, should.HaveSameElementsDeep, []string{ + "mac_state", + "pending_mac_state", + "pending_session", + "recent_adr_uplinks", + "recent_uplinks", + "session", }) - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for join-request to be sent to AS") + macState := makeMACState() + macState.RxWindowsAvailable = true + a.So(dev.MACState, should.Resemble, macState) + a.So(dev.PendingMACState, should.BeNil) + a.So(dev.PendingSession, should.BeNil) + a.So(dev.RecentADRUplinks, should.BeNil) + a.So(dev.Session, should.Resemble, makeSession(ttnpb.MAC_V1_0_2, devAddr, 34)) + if a.So(dev.RecentUplinks, should.NotBeEmpty) { + recentUp = dev.RecentUplinks[len(dev.RecentUplinks)-1] + a.So([]time.Time{start, recentUp.ReceivedAt, time.Now()}, should.BeChronological) + a.So(recentUp.RxMetadata, should.HaveSameElementsDiff, mds) + expectedUp := makeLegacyDataUplink(34, true) + expectedUp.CorrelationIDs = upCorrelationIDs + expectedUp.DeviceChannelIndex = 1 + expectedUp.ReceivedAt = recentUp.ReceivedAt + expectedUp.RxMetadata = recentUp.RxMetadata + expectedUp.Settings.DataRateIndex = ttnpb.DATA_RATE_2 + a.So(dev.RecentUplinks, should.HaveEmptyDiff, append(CopyUplinkMessages(rangeDevice.RecentUplinks...), expectedUp)) + } + req.Response <- DeviceRegistrySetByIDResponse{ + Device: &ttnpb.EndDevice{ + EndDeviceIdentifiers: *makeOTAAIdentifiers(&devAddr), + FrequencyPlanID: test.EUFrequencyPlanID, + LoRaWANPHYVersion: ttnpb.PHY_V1_0_2_REV_B, + LoRaWANVersion: ttnpb.MAC_V1_0_2, + MACState: macState, + RecentUplinks: []*ttnpb.UplinkMessage{ + makeLegacyDataUplink(31, true), + makeLegacyDataUplink(32, true), + makeLegacyDataUplink(34, true), + makeLegacyDataUplink(34, true), + }, + Session: makeSession(ttnpb.MAC_V1_0_2, devAddr, 34), + CreatedAt: start, + UpdatedAt: time.Now(), + }, + } } - select { - case req := <-downlinkAddCh: - a.So(req.ctx, should.HaveParentContext, ctx) - a.So(req.devID, should.Resemble, pb.EndDeviceIdentifiers) - a.So(req.replace, should.BeTrue) - a.So([]time.Time{start, req.t, time.Now()}, should.BeChronological) - - case <-time.After(Timeout): - t.Fatal("Timeout waiting for Add to be called") + if !a.So(AssertDownlinkTaskAddRequest(ctx, env.DownlinkTasks.Add, func(ctx context.Context, ids ttnpb.EndDeviceIdentifiers, startAt time.Time, replace bool) bool { + return a.So(ctx, should.HaveParentContextOrEqual, upCtx) && + a.So(ids, should.Resemble, *makeOTAAIdentifiers(&devAddr)) && + a.So(startAt, should.Resemble, recentUp.ReceivedAt.Add(time.Second-NSScheduleWindow())) && + a.So(replace, should.BeTrue) + }, + nil, + ), should.BeTrue) { + return false } - _ = sendUplinkDuplicates(t, ns, collectionDoneCh, ctx, tc.UplinkMessage, DuplicateCount) + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtMergeMetadata(upCtx, rangeDevice.EndDeviceIdentifiers, len(mds))) + }), should.BeTrue) { + return false + } - select { - case err := <-errch: - a.So(err, should.BeNil) + _ = sendUplinkDuplicates(ctx, handle, env.CollectionDone, func(decoded bool) *ttnpb.UplinkMessage { + msg := makeLegacyDataUplink(34, decoded) + if !decoded { + return msg + } + msg.DeviceChannelIndex = 1 + msg.Settings.DataRateIndex = ttnpb.DATA_RATE_2 + return msg + }, start, duplicateCount) - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for HandleUplink to return") - } + return assertHandleUplinkResponse(ctx, handleUplinkErrCh, func(err error) bool { + return a.So(err, should.BeNil) + }) + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + handleTest := func(ctx context.Context, ns *NetworkServer, env TestEnvironment, asRecvCh <-chan AsNsLinkRecvRequest, stop func()) { + defer stop() - t.Run("after cooldown window", func(t *testing.T) { - a := assertions.New(t) + <-env.DownlinkTasks.Pop - errch := make(chan error, 1) + if !tc.Handler(ctx, env, asRecvCh, func(ctx context.Context, msg *ttnpb.UplinkMessage) <-chan error { + ch := make(chan error) go func() { - _, err = ns.HandleUplink(ctx, CopyUplinkMessage(tc.UplinkMessage)) - errch <- err - }() - - select { - case req := <-handleJoinCh: - if ses := tc.Device.Session; ses != nil { - a.So(req.req.DevAddr, should.NotResemble, ses.DevAddr) + _, err := ttnpb.NewGsNsClient(ns.LoopbackConn()).HandleUplink(ctx, CopyUplinkMessage(msg)) + ttnErr, ok := errors.From(err) + if ok { + ch <- ttnErr + } else { + ch <- err } - a.So(req.req.CorrelationIDs, should.HaveLength, 1) - - expectedRequest.DevAddr = req.req.DevAddr - expectedRequest.CorrelationIDs = req.req.CorrelationIDs - a.So(req.req, should.Resemble, expectedRequest) + close(ch) + }() + return ch + }) { + t.Error("Test handler failed") + } + } - resp := &ttnpb.JoinResponse{ - RawPayload: bytes.Repeat([]byte{42}, 17), - SessionKeys: *keys, - } + makeConfig := func() Config { + return Config{ + NetID: *netID.Copy(&types.NetID{}), + DefaultMACSettings: MACSettingConfig{}, + } + } - req.ch <- resp - req.errch <- nil + timeout := (1 << 12) * test.Delay - case err := <-errch: - a.So(err, should.BeNil) - t.Fatal("Join not sent to JS") + t.Run("no link", func(t *testing.T) { + ns, ctx, env, stop := StartTest(t, makeConfig(), timeout) + handleTest(ctx, ns, env, nil, func() { + defer stop() + assertions.New(t).So(AssertNetworkServerClose(ctx, ns), should.BeTrue) + }) + }) - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for join-request to be sent to JS") - } + t.Run("active link", func(t *testing.T) { + ns, ctx, env, stop := StartTest(t, makeConfig(), timeout) - _ = sendUplinkDuplicates(t, ns, deduplicationDoneCh, ctx, tc.UplinkMessage, DuplicateCount) + link, ok := AssertLinkApplication(ctx, ns.LoopbackConn(), env.Cluster.GetPeer, appID) + if !ok { + t.Fatal("Failed to link application") + } - pb, err = devReg.GetByID(ctx, pb.EndDeviceIdentifiers.ApplicationIdentifiers, pb.EndDeviceIdentifiers.DeviceID, ttnpb.EndDeviceFieldPathsTopLevel) - if !a.So(err, should.BeNil) { - t.Fatalf("Failed to get device from registry: %s", err) - } + a := assertions.New(t) - select { - case up := <-asSendCh: - a.So(up.CorrelationIDs, should.NotBeEmpty) - a.So(up, should.HaveEmptyDiff, &ttnpb.ApplicationUp{ - CorrelationIDs: up.CorrelationIDs, - EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ - DevAddr: &expectedRequest.DevAddr, - DevEUI: tc.Device.EndDeviceIdentifiers.DevEUI, - DeviceID: tc.Device.EndDeviceIdentifiers.DeviceID, - JoinEUI: tc.Device.EndDeviceIdentifiers.JoinEUI, - ApplicationIdentifiers: tc.Device.EndDeviceIdentifiers.ApplicationIdentifiers, - }, - ReceivedAt: up.ReceivedAt, - Up: &ttnpb.ApplicationUp_JoinAccept{JoinAccept: &ttnpb.ApplicationJoinAccept{ - AppSKey: resp.SessionKeys.AppSKey, - SessionKeyID: resp.SessionKeys.SessionKeyID, - }}, - }) + var evCorrelationIDs []string + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + evCorrelationIDs = ev.CorrelationIDs() + return a.So(evCorrelationIDs, should.HaveLength, 1) && + a.So(ev, should.ResembleEvent, EvtBeginApplicationLink(events.ContextWithCorrelationID(ctx, evCorrelationIDs...), appID, nil)) + }), should.BeTrue) { + t.FailNow() + } - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for join-request to be sent to AS") - } + asRecvCh := make(chan AsNsLinkRecvRequest) + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for { + up, err := link.Recv() + if err != nil { + t.Logf("Receive on AS link returned error: %v", err) + close(asRecvCh) + return + } - pb.EndDeviceIdentifiers.DevAddr = &expectedRequest.DevAddr - select { - case req := <-downlinkAddCh: - a.So(req.ctx, should.HaveParentContext, ctx) - a.So(req.devID, should.Resemble, tc.Device.EndDeviceIdentifiers) - a.So(req.replace, should.BeTrue) - a.So([]time.Time{start, req.t, time.Now()}, should.BeChronological) + respCh := make(chan *pbtypes.Empty) + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for AS uplink to be processed") + return + case asRecvCh <- AsNsLinkRecvRequest{ + Uplink: up, + Response: respCh, + }: + } - case <-time.After(Timeout): - t.Fatal("Timeout waiting for Add to be called") + select { + case <-ctx.Done(): + t.Error("Timed out while waiting for AS uplink response to be processed") + return + case resp := <-respCh: + if err := link.Send(resp); err != nil { + t.Logf("Send on the link returned error: %v", err) + } + } } + }() + handleTest(ctx, ns, env, asRecvCh, func() { + defer stop() - _ = sendUplinkDuplicates(t, ns, collectionDoneCh, ctx, tc.UplinkMessage, DuplicateCount) - - select { - case err := <-errch: - a.So(err, should.BeNil) - - case <-time.After(Timeout): - t.Fatal("Timed out while waiting for HandleUplink to return") - } + wg.Add(1) + go func() { + defer wg.Done() + if !a.So(test.AssertEventPubSubPublishRequest(ctx, env.Events, func(ev events.Event) bool { + return a.So(ev, should.ResembleEvent, EvtEndApplicationLink(events.ContextWithCorrelationID(ctx, evCorrelationIDs...), appID, nil)) + }), should.BeTrue) { + return + } + }() + a.So(AssertNetworkServerClose(ctx, ns), should.BeTrue) + wg.Wait() // prevent panic when assertions in goroutines fail }) }) - } - } -} - -func handleRejoinTest() func(t *testing.T) { - return func(t *testing.T) { - // TODO: Implement https://github.com/TheThingsNetwork/lorawan-stack/issues/8 + }) } } - -func TestHandleUplink(t *testing.T) { - a := assertions.New(t) - - authorizedCtx := clusterauth.NewContext(test.Context(), nil) - - redisClient, flush := test.NewRedis(t, "networkserver_test") - defer flush() - defer redisClient.Close() - devReg := &redis.DeviceRegistry{Redis: redisClient} - - ns := test.Must(New( - component.MustNew(test.GetLogger(t), &component.Config{}), - &Config{ - Devices: devReg, - DeduplicationWindow: 42, - CooldownWindow: 42, - DownlinkTasks: &MockDownlinkTaskQueue{ - PopFunc: DownlinkTaskPopBlockFunc, - }, - }, - )).(*NetworkServer) - test.Must(nil, ns.Start()) - defer ns.Close() - - msg := ttnpb.NewPopulatedUplinkMessage(test.Randy, false) - msg.Payload.Payload = nil - msg.RawPayload = nil - _, err := ns.HandleUplink(authorizedCtx, msg) - a.So(err, should.NotBeNil) - - msg = ttnpb.NewPopulatedUplinkMessage(test.Randy, false) - msg.Payload.Payload = nil - msg.RawPayload = []byte{} - _, err = ns.HandleUplink(authorizedCtx, msg) - a.So(err, should.NotBeNil) - - msg = ttnpb.NewPopulatedUplinkMessage(test.Randy, false) - msg.Payload.Major = 1 - _, err = ns.HandleUplink(authorizedCtx, msg) - a.So(err, should.NotBeNil) - - t.Run("Uplink", handleUplinkTest()) - t.Run("Join", handleJoinTest()) - t.Run("Rejoin", handleRejoinTest()) -}