diff --git a/drivers/net/wireless/intel/iwlwifi/mld/tests/rx.c b/drivers/net/wireless/intel/iwlwifi/mld/tests/rx.c
new file mode 100644
index 000000000000..20cb4e03ab41
--- /dev/null
+++ b/drivers/net/wireless/intel/iwlwifi/mld/tests/rx.c
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * KUnit tests for channel helper functions
+ *
+ * Copyright (C) 2024 Intel Corporation
+ */
+#include <kunit/test.h>
+#include "utils.h"
+#include "iwl-trans.h"
+#include "mld.h"
+#include "sta.h"
+
+static const struct is_dup_case {
+	const char *desc;
+	struct {
+		/* ieee80211_hdr fields */
+		__le16 fc;
+		__le16 seq;
+		u8 tid;
+		bool multicast;
+		/* iwl_rx_mpdu_desc fields */
+		bool is_amsdu;
+		u8 sub_frame_idx;
+	} rx_pkt;
+	struct {
+		__le16 last_seq;
+		u8 last_sub_frame_idx;
+		u8 tid;
+	} dup_data_state;
+	struct {
+		bool is_dup;
+		u32 rx_status_flag;
+	} result;
+} is_dup_cases[] = {
+	{
+		.desc = "Control frame",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_CTL),
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = 0,
+		}
+	},
+	{
+		.desc = "Null func frame",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_NULLFUNC),
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = 0,
+		}
+	},
+	{
+		.desc = "Multicast data",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA),
+			.multicast = true,
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = 0,
+		}
+	},
+	{
+		.desc = "QoS null func frame",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_NULLFUNC),
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = 0,
+		}
+	},
+	{
+		.desc = "QoS data new sequence",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_DATA),
+			.seq = __cpu_to_le16(0x101),
+		},
+		.dup_data_state = {
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = RX_FLAG_DUP_VALIDATED,
+		},
+	},
+	{
+		.desc = "QoS data same sequence, no retry",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_DATA),
+			.seq = __cpu_to_le16(0x100),
+		},
+		.dup_data_state = {
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = RX_FLAG_DUP_VALIDATED,
+		},
+	},
+	{
+		.desc = "QoS data same sequence, has retry",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_DATA |
+					    IEEE80211_FCTL_RETRY),
+			.seq = __cpu_to_le16(0x100),
+		},
+		.dup_data_state = {
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = true,
+			.rx_status_flag = 0,
+		},
+	},
+	{
+		.desc = "QoS data invalid tid",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_DATA),
+			.seq = __cpu_to_le16(0x100),
+			.tid = IWL_MAX_TID_COUNT + 1,
+		},
+		.result = {
+			.is_dup = true,
+			.rx_status_flag = 0,
+		},
+	},
+	{
+		.desc = "non-QoS data, same sequence, same tid, no retry",
+		.rx_pkt = {
+			/* Driver will use tid = IWL_MAX_TID_COUNT */
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA),
+			.seq = __cpu_to_le16(0x100),
+		},
+		.dup_data_state = {
+			.tid = IWL_MAX_TID_COUNT,
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = RX_FLAG_DUP_VALIDATED,
+		},
+	},
+	{
+		.desc = "non-QoS data, same sequence, same tid, has retry",
+		.rx_pkt = {
+			/* Driver will use tid = IWL_MAX_TID_COUNT */
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_FCTL_RETRY),
+			.seq = __cpu_to_le16(0x100),
+		},
+		.dup_data_state = {
+			.tid = IWL_MAX_TID_COUNT,
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = true,
+			.rx_status_flag = 0,
+		},
+	},
+	{
+		.desc = "non-QoS data, same sequence on different tid's",
+		.rx_pkt = {
+			/* Driver will use tid = IWL_MAX_TID_COUNT */
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA),
+			.seq = __cpu_to_le16(0x100),
+		},
+		.dup_data_state = {
+			.tid = 7,
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = RX_FLAG_DUP_VALIDATED,
+		},
+	},
+	{
+		.desc = "A-MSDU new subframe, allow same PN",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_DATA),
+			.seq = __cpu_to_le16(0x100),
+			.is_amsdu = true,
+			.sub_frame_idx = 1,
+		},
+		.dup_data_state = {
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = RX_FLAG_ALLOW_SAME_PN |
+					  RX_FLAG_DUP_VALIDATED,
+		},
+	},
+	{
+		.desc = "A-MSDU subframe with smaller idx, disallow same PN",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_DATA),
+			.seq = __cpu_to_le16(0x100),
+			.is_amsdu = true,
+			.sub_frame_idx = 1,
+		},
+		.dup_data_state = {
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 2,
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = RX_FLAG_DUP_VALIDATED,
+		},
+	},
+	{
+		.desc = "A-MSDU same subframe, no retry, disallow same PN",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_DATA),
+			.seq = __cpu_to_le16(0x100),
+			.is_amsdu = true,
+			.sub_frame_idx = 0,
+		},
+		.dup_data_state = {
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = false,
+			.rx_status_flag = RX_FLAG_DUP_VALIDATED,
+		},
+	},
+	{
+		.desc = "A-MSDU same subframe, has retry",
+		.rx_pkt = {
+			.fc = __cpu_to_le16(IEEE80211_FTYPE_DATA |
+					    IEEE80211_STYPE_QOS_DATA |
+					    IEEE80211_FCTL_RETRY),
+			.seq = __cpu_to_le16(0x100),
+			.is_amsdu = true,
+			.sub_frame_idx = 0,
+		},
+		.dup_data_state = {
+			.last_seq = __cpu_to_le16(0x100),
+			.last_sub_frame_idx = 0,
+		},
+		.result = {
+			.is_dup = true,
+			.rx_status_flag = 0,
+		},
+	},
+};
+
+KUNIT_ARRAY_PARAM_DESC(test_is_dup, is_dup_cases, desc);
+
+static void
+setup_dup_data_state(struct ieee80211_sta *sta)
+{
+	struct kunit *test = kunit_get_current_test();
+	const struct is_dup_case *param = (const void *)(test->param_value);
+	struct iwl_mld_sta *mld_sta = iwl_mld_sta_from_mac80211(sta);
+	u8 tid = param->dup_data_state.tid;
+	struct iwl_mld_rxq_dup_data *dup_data;
+
+	/* Allocate dup_data only for 1 queue */
+	KUNIT_ALLOC_AND_ASSERT(test, dup_data);
+
+	/* Initialize dup data, see iwl_mld_alloc_dup_data */
+	memset(dup_data->last_seq, 0xff, sizeof(dup_data->last_seq));
+
+	dup_data->last_seq[tid] = param->dup_data_state.last_seq;
+	dup_data->last_sub_frame_idx[tid] =
+		param->dup_data_state.last_sub_frame_idx;
+
+	mld_sta->dup_data = dup_data;
+}
+
+static void setup_rx_pkt(const struct is_dup_case *param,
+			 struct ieee80211_hdr *hdr,
+			 struct iwl_rx_mpdu_desc *mpdu_desc)
+{
+	u8 tid = param->rx_pkt.tid;
+
+	/* Set "new rx packet" header */
+	hdr->frame_control = param->rx_pkt.fc;
+	hdr->seq_ctrl = param->rx_pkt.seq;
+
+	if (ieee80211_is_data_qos(hdr->frame_control)) {
+		u8 *qc = ieee80211_get_qos_ctl(hdr);
+
+		qc[0] = tid & IEEE80211_QOS_CTL_TID_MASK;
+	}
+
+	if (param->rx_pkt.multicast)
+		hdr->addr1[0] = 0x1;
+
+	/* Set mpdu_desc */
+	mpdu_desc->amsdu_info = param->rx_pkt.sub_frame_idx &
+				IWL_RX_MPDU_AMSDU_SUBFRAME_IDX_MASK;
+	if (param->rx_pkt.is_amsdu)
+		mpdu_desc->mac_flags2 |= IWL_RX_MPDU_MFLG2_AMSDU;
+}
+
+static void test_is_dup(struct kunit *test)
+{
+	const struct is_dup_case *param = (const void *)(test->param_value);
+	struct iwl_mld *mld = test->priv;
+	struct iwl_rx_mpdu_desc mpdu_desc = { };
+	struct ieee80211_rx_status rx_status = { };
+	struct ieee80211_vif *vif;
+	struct ieee80211_sta *sta;
+	struct ieee80211_hdr hdr;
+
+	vif = iwlmld_kunit_add_vif(false, NL80211_IFTYPE_STATION);
+	sta = iwlmld_kunit_setup_sta(vif, IEEE80211_STA_AUTHORIZED, -1);
+
+	/* Prepare test case state */
+	setup_dup_data_state(sta);
+	setup_rx_pkt(param, &hdr, &mpdu_desc);
+
+	KUNIT_EXPECT_EQ(test,
+			iwl_mld_is_dup(mld, sta, &hdr, &mpdu_desc, &rx_status,
+				       0), /* assuming only 1 queue */
+			param->result.is_dup);
+	KUNIT_EXPECT_EQ(test, rx_status.flag, param->result.rx_status_flag);
+}
+
+static struct kunit_case is_dup_test_cases[] = {
+	KUNIT_CASE_PARAM(test_is_dup, test_is_dup_gen_params),
+	{},
+};
+
+static struct kunit_suite is_dup = {
+	.name = "iwlmld-rx-is-dup",
+	.test_cases = is_dup_test_cases,
+	.init = iwlmld_kunit_test_init,
+};
+
+kunit_test_suite(is_dup);
