func handleDeviceRegistryTest(t *testing.T, reg DeviceRegistry) {
	a, ctx := test.New(t)
	pb := &ttnpb.EndDevice{
		EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{
			JoinEui:                &types.EUI64{0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
			DevEui:                 &types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
			ApplicationIdentifiers: ttnpb.ApplicationIdentifiers{ApplicationId: "test-app"},
			DeviceId:               "test-dev",
		},
		ProvisionerId: "mock",
		ProvisioningData: &pbtypes.Struct{
			Fields: map[string]*pbtypes.Value{
				"serial_number": {
					Kind: &pbtypes.Value_NumberValue{
						NumberValue: 42,
					},
				},
			},
		},
	}
	retCtx, err := reg.GetByEUI(ctx, *pb.EndDeviceIdentifiers.JoinEui, *pb.EndDeviceIdentifiers.DevEui, ttnpb.EndDeviceFieldPathsTopLevel)
	if !a.So(err, should.NotBeNil) || !a.So(errors.IsNotFound(err), should.BeTrue) {
		t.Fatalf("Error received: %v", err)
	}
	a.So(retCtx, should.BeNil)
	start := time.Now()
	ret, err := reg.SetByID(ctx, pb.ApplicationIdentifiers, pb.DeviceId,
		[]string{
			"provisioner_id",
			"provisioning_data",
		},
		func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) {
			if !a.So(stored, should.BeNil) {
				t.Fatal("Registry is not empty")
			}
			return CopyEndDevice(pb), []string{
				"ids.application_ids",
				"ids.dev_eui",
				"ids.device_id",
				"ids.join_eui",
				"provisioner_id",
				"provisioning_data",
			}, nil
		},
	)
	if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) {
		t.Fatalf("Failed to create device: %s", err)
	}
	a.So(ret.CreatedAt, should.HappenAfter, start)
	a.So(ret.UpdatedAt, should.HappenAfter, start)
	a.So(ret.UpdatedAt, should.Equal, ret.CreatedAt)
	pb.CreatedAt = ret.CreatedAt
	pb.UpdatedAt = ret.UpdatedAt
	a.So(ret, should.HaveEmptyDiff, pb)
	retCtx, err = reg.GetByEUI(ctx, *pb.EndDeviceIdentifiers.JoinEui, *pb.EndDeviceIdentifiers.DevEui, ttnpb.EndDeviceFieldPathsTopLevel)
	if !a.So(err, should.BeNil) {
		t.Fatalf("Failed to get device: %s", err)
	}
	a.So(retCtx.EndDevice, should.HaveEmptyDiff, pb)
	pbOther := CopyEndDevice(pb)
	pbOther.DeviceId = "other-device"
	pbOther.DevEui = &types.EUI64{0x43, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
	retCtx, err = reg.GetByEUI(ctx, *pbOther.EndDeviceIdentifiers.JoinEui, *pbOther.EndDeviceIdentifiers.DevEui, ttnpb.EndDeviceFieldPathsTopLevel)
	if !a.So(err, should.NotBeNil) || !a.So(errors.IsNotFound(err), should.BeTrue) {
		t.Fatalf("Error received: %v", err)
	}
	a.So(retCtx, should.BeNil)
	_, err = reg.SetByID(ctx, pbOther.ApplicationIdentifiers, pbOther.DeviceId,
		[]string{
			"provisioner_id",
			"provisioning_data",
		},
		func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) {
			if !a.So(stored, should.BeNil) {
				t.Fatal("Registry is not empty")
			}
			return CopyEndDevice(pbOther), []string{
				"ids.application_ids",
				"ids.dev_eui",
				"ids.device_id",
				"ids.join_eui",
				"provisioner_id",
				"provisioning_data",
			}, nil
		},
	)
	if !a.So(errors.IsAlreadyExists(err), should.BeTrue) {
		t.Fatal("Device with conflicting provisioner unique ID created")
	}
	err = DeleteDevice(ctx, reg, pb.ApplicationIdentifiers, pb.DeviceId)
	if !a.So(err, should.BeNil) {
		t.FailNow()
	}
	retCtx, err = reg.GetByEUI(ctx, *pb.EndDeviceIdentifiers.JoinEui, *pb.EndDeviceIdentifiers.DevEui, ttnpb.EndDeviceFieldPathsTopLevel)
	if !a.So(err, should.NotBeNil) || !a.So(errors.IsNotFound(err), should.BeTrue) {
		t.Fatalf("Error received: %v", err)
	}
	a.So(retCtx, should.BeNil)
	ret, err = reg.SetByID(ctx, pbOther.ApplicationIdentifiers, pbOther.DeviceId,
		[]string{
			"provisioner_id",
			"provisioning_data",
		},
		func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) {
			if !a.So(stored, should.BeNil) {
				t.Fatal("Registry is not empty")
			}
			return CopyEndDevice(pbOther), []string{
				"ids.application_ids",
				"ids.dev_eui",
				"ids.device_id",
				"ids.join_eui",
				"provisioner_id",
				"provisioning_data",
			}, nil
		},
	)
	if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) {  
		t.Fatalf("Failed to create device: %s", err)
	}
	a.So(ret.CreatedAt, should.HappenAfter, pb.CreatedAt)
	a.So(ret.UpdatedAt, should.HappenAfter, pb.UpdatedAt)
	a.So(ret.UpdatedAt, should.Equal, ret.CreatedAt)
	pbOther.CreatedAt = ret.CreatedAt
	pbOther.UpdatedAt = ret.UpdatedAt
	a.So(ret, should.HaveEmptyDiff, pbOther)
	retCtx, err = reg.GetByEUI(ctx, *pbOther.EndDeviceIdentifiers.JoinEui, *pbOther.EndDeviceIdentifiers.DevEui, ttnpb.EndDeviceFieldPathsTopLevel)
	if !a.So(err, should.BeNil) {
		t.FailNow()
	}
	a.So(retCtx.EndDevice, should.HaveEmptyDiff, pbOther)
	err = DeleteDevice(ctx, reg, pbOther.ApplicationIdentifiers, pbOther.DeviceId)
	if !a.So(err, should.BeNil) {
		t.FailNow()
	}
	retCtx, err = reg.GetByEUI(ctx, *pbOther.EndDeviceIdentifiers.JoinEui, *pbOther.EndDeviceIdentifiers.DevEui, ttnpb.EndDeviceFieldPathsTopLevel)
	if !a.So(err, should.NotBeNil) || !a.So(errors.IsNotFound(err), should.BeTrue) {
		t.Fatalf("Error received: %v", err)
	}
	a.So(retCtx, should.BeNil)
}
handleDeviceRegistryTest runs a test suite on reg.
package joinserver_test
import (
	"context"
	"fmt"
	"testing"
	"time"
	pbtypes "github.com/gogo/protobuf/types"
	"github.com/mohae/deepcopy"
	"github.com/smartystreets/assertions"
	"go.thethings.network/lorawan-stack/v3/pkg/errors"
	. "go.thethings.network/lorawan-stack/v3/pkg/joinserver"
	"go.thethings.network/lorawan-stack/v3/pkg/joinserver/redis"
	"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
	"go.thethings.network/lorawan-stack/v3/pkg/types"
	"go.thethings.network/lorawan-stack/v3/pkg/util/test"
	"go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should"
)
func CopyEndDevice(pb *ttnpb.EndDevice) *ttnpb.EndDevice {
	return deepcopy.Copy(pb).(*ttnpb.EndDevice)
}