diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/pom.xml b/components/org.wso2.carbon.identity.notification.push.device.handler/pom.xml
index a992db8..ef6e1bb 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/pom.xml
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/pom.xml
@@ -51,6 +51,10 @@
org.wso2.carbon.identity.organization.management.core
org.wso2.carbon.identity.organization.management.service
+
+ org.wso2.carbon.identity.event.handler.notification
+ org.wso2.carbon.identity.event.handler.notification
+
org.wso2.carbon.identity.event.handler.notification
org.wso2.carbon.identity.notification.sender.tenant.config
@@ -95,6 +99,10 @@
org.wso2.carbon.identity.framework
org.wso2.carbon.identity.central.log.mgt
+
+ org.wso2.carbon.identity.framework
+ org.wso2.carbon.identity.configuration.mgt.core
+
org.wso2.orbit.com.google.firebase
firebase-admin
@@ -139,12 +147,19 @@
org.json,
org.wso2.carbon.identity.central.log.mgt.utils;
version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.configuration.mgt.core; version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.configuration.mgt.core.exception; version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.configuration.mgt.core.model; version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.configuration.mgt.core.constant; version="${carbon.identity.framework.imp.pkg.version.range}",
org.apache.commons.logging; version="${org.apache.commons.logging.range}",
+ org.apache.commons.collections,
org.osgi.framework; version="${osgi.framework.imp.pkg.version.range}",
org.osgi.service.component; version="${osgi.service.component.imp.pkg.version.range}",
org.wso2.carbon.identity.base; version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.core; version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.core.cache; version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.core.context; version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.core.context.model; version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.core.util; version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.organization.management.service;version="${org.wso2.carbon.identity.organization.management.core.version.range}",
org.wso2.carbon.identity.organization.management.service.exception;version="${org.wso2.carbon.identity.organization.management.core.version.range}",
@@ -168,6 +183,10 @@
org.wso2.carbon.user.core.common; version="${carbon.kernel.package.import.version.range}",
org.wso2.carbon; version="${carbon.kernel.package.import.version.range}",
org.wso2.carbon.context; version="${carbon.kernel.package.import.version.range}",
+ org.wso2.carbon.identity.event; version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.event.event; version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.event.services; version="${carbon.identity.framework.imp.pkg.version.range}",
+ org.wso2.carbon.identity.governance.service.notification; version="${identity.governance.imp.pkg.version.range}",
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/DeviceHandlerService.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/DeviceHandlerService.java
index dbab23e..66674d8 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/DeviceHandlerService.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/DeviceHandlerService.java
@@ -23,6 +23,9 @@
import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationDiscoveryData;
import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationRequest;
+import java.util.Collections;
+import java.util.List;
+
/**
* Device Handler interface.
*/
@@ -60,6 +63,7 @@ Device registerDevice(RegistrationRequest registrationRequest, String tenantDoma
* Remove all devices of a registered user.
*
* @param userId User ID.
+ * @param tenantDomain Tenant Domain.
* @throws PushDeviceHandlerException Push Device Handler Exception.
*/
void unregisterDeviceByUserId(String userId, String tenantDomain) throws PushDeviceHandlerException;
@@ -77,11 +81,25 @@ Device registerDevice(RegistrationRequest registrationRequest, String tenantDoma
* Get a device by the user ID.
*
* @param userId User ID.
+ * @param tenantDomain Tenant Domain.
* @return Device.
* @throws PushDeviceHandlerException Push Device Handler Exception.
*/
Device getDeviceByUserId(String userId, String tenantDomain) throws PushDeviceHandlerException;
+ /**
+ * Get devices by the user ID.
+ *
+ * @param userId User ID.
+ * @param tenantDomain Tenant Domain.
+ * @return Device.
+ * @throws PushDeviceHandlerException Push Device Handler Exception.
+ */
+ default List getDevicesByUserId(String userId, String tenantDomain) throws PushDeviceHandlerException {
+
+ return Collections.emptyList();
+ }
+
/**
* Edit the name of a registered device.
*
@@ -111,5 +129,4 @@ RegistrationDiscoveryData getRegistrationDiscoveryData(String username, String t
* @throws PushDeviceHandlerException Push Device Handler Exception.
*/
String getPublicKey(String deviceId) throws PushDeviceHandlerException;
-
}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstants.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstants.java
index 7a6f958..3ce7866 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstants.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstants.java
@@ -31,6 +31,104 @@ public class PushDeviceHandlerConstants {
"PushAuthenticator.DeviceRegistrationContext.ValidityPeriod";
public static final int DEFAULT_DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD = 180;
+ public static final String MAX_DEVICE_LIMIT_PER_USER = "PushAuthenticator.DeviceManagement.MaxDeviceLimitPerUser";
+ public static final int DEFAULT_MAX_DEVICE_LIMIT_PER_USER = 10;
+ public static final int DEFAULT_MIN_DEVICE_LIMIT_PER_USER = 2;
+ public static final String PUSH_DEVICE_MGT_RESOURCE_TYPE = "DEVICE_MANAGEMENT";
+ public static final String PUSH_DEVICE_MGT_RESOURCE_NAME = "PUSH_DEVICE_MANAGEMENT";
+ public static final String ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT = "enableMultipleDeviceEnrollment";
+ public static final String ATTR_MAX_DEVICE_LIMIT = "maximumDeviceLimit";
+ public static final String ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS = "enableDeviceRegistrationNotifications";
+ public static final String ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS =
+ "deviceRegistrationNotificationChannels";
+ public static final String NOTIFICATION_CHANNELS_SEPARATOR = ",";
+ public static final String REGISTRATION_TIME_FORMATTER_PATTERN = "yyyy-MM-dd HH:mm:ss 'UTC'";
+
+
+
+ /**
+ * Constants related to email notifications sent during device management operations.
+ */
+ public static class EmailNotificationConstants {
+
+ public static final String PUSH_DEVICE_REGISTRATION_TEMPLATE = "PushDeviceRegistration";
+
+ // Placeholders used in the template body
+ public static final String DEVICE_NAME_PLACEHOLDER = "push-device-name";
+ public static final String DEVICE_MODEL_PLACEHOLDER = "push-device-model";
+ public static final String REGISTRATION_TIME_PLACEHOLDER = "registration-time";
+ public static final String IP_ADDRESS_PLACEHOLDER = "ip-address";
+ }
+
+ /**
+ * Constants related to push notifications sent during device management operations.
+ */
+ public static final class PushNotificationConstants {
+
+ public static final String PUSH_NOTIFICATION_EVENT_NAME = "TRIGGER_PUSH_NOTIFICATION";
+ public static final String PUSH_NOTIFICATION_CHANNEL = "PUSH_NOTIFICATION";
+ public static final String NOTIFICATION_SCENARIO = "NOTIFICATION_SCENARIO";
+ public static final String DEVICE_REGISTRATION_SCENARIO = "DEVICE_REGISTRATION";
+ public static final String NOTIFICATION_PROVIDER = "notificationProvider";
+ public static final String DEVICE_TOKEN = "deviceToken";
+ public static final String DEVICE_ID = "deviceId";
+ public static final String DEVICE_HANDLE = "deviceHandle";
+ public static final String IP_ADDRESS = "ipAddress";
+
+ // Placeholders rendered in the push notification template.
+ public static final String DEVICE_NAME_PLACEHOLDER = "push-device-name";
+ public static final String DEVICE_MODEL_PLACEHOLDER = "push-device-model";
+ public static final String REGISTRATION_TIME_PLACEHOLDER = "registration-time";
+
+ /**
+ * Private constructor to prevent initialization of the class.
+ */
+ private PushNotificationConstants() {
+
+ }
+ }
+
+ /**
+ * Constants related to diagnostic logging of device management operations.
+ */
+ public static class LogConstants {
+
+ public static final String PUSH_DEVICE_HANDLER_SERVICE = "push-device-handler-service";
+
+ /**
+ * Action identifiers used in diagnostic logs.
+ */
+ public static class ActionIDs {
+
+ public static final String TRIGGER_DEVICE_REGISTRATION_EMAIL_NOTIFICATION =
+ "trigger-device-registration-email-notification";
+ public static final String TRIGGER_DEVICE_REGISTRATION_PUSH_NOTIFICATION =
+ "trigger-device-registration-push-notification";
+
+ private ActionIDs() {
+
+ }
+ }
+
+ /**
+ * Input parameter keys used in diagnostic logs.
+ */
+ public static class InputKeys {
+
+ public static final String DEVICE_ID = "device-id";
+ public static final String USER_ID = "user-id";
+ public static final String TENANT_DOMAIN = "tenant-domain";
+
+ private InputKeys() {
+
+ }
+ }
+
+ private LogConstants() {
+
+ }
+ }
+
/**
* Private constructor to prevent initialization of the class.
*/
@@ -136,6 +234,38 @@ public enum ErrorMessages {
ERROR_CODE_FAILED_TO_RESOLVE_PUSH_PROVIDER(
"PDH-15013",
"Failed to resolve the correct push provider for the request."
+ ),
+ ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG(
+ "PDH-15014",
+ "Failed to get push device configuration for the tenant."
+ ),
+ ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG(
+ "PDH-15015",
+ "Failed to add push device configuration for the tenant."
+ ),
+ ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG(
+ "PDH-15016",
+ "Failed to update push device configuration for the tenant."
+ ),
+ ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG(
+ "PDH-15017",
+ "Maximum device limit can only be updated when multiple device enrollment is enabled."
+ ),
+ ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS(
+ "PDH-15018",
+ "Maximum device limit cannot exceed the server-configured device registration per user."
+ ),
+ ERROR_CODE_MAX_DEVICE_LIMIT_REACHED(
+ "PDH-15019",
+ "Maximum device limit reached for the user: %s."
+ ),
+ ERROR_CODE_DEVICE_ID_ALREADY_REGISTERED(
+ "PDH-15020",
+ "Device is already registered for the device ID: %s."
+ ),
+ ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE(
+ "PDH-15021",
+ "Maximum device limit must be a positive value when multiple device enrollment is enabled."
);
private final String code;
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAO.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAO.java
index d63842c..e652ddf 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAO.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAO.java
@@ -18,9 +18,11 @@
package org.wso2.carbon.identity.notification.push.device.handler.dao;
+import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerException;
import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerServerException;
import org.wso2.carbon.identity.notification.push.device.handler.model.Device;
+import java.util.List;
import java.util.Optional;
/**
@@ -33,9 +35,9 @@ public interface DeviceDAO {
*
* @param device Device to be registered.
* @param tenantId Tenant ID.
- * @throws PushDeviceHandlerServerException PushDeviceHandlerServerException.
+ * @throws PushDeviceHandlerException PushDeviceHandlerException.
*/
- void registerDevice(Device device, int tenantId) throws PushDeviceHandlerServerException;
+ void registerDevice(Device device, int tenantId) throws PushDeviceHandlerException;
/**
* Unregister a device.
@@ -67,11 +69,22 @@ public interface DeviceDAO {
* Get a device by the user ID.
*
* @param userId User ID.
+ * @param tenantId Tenant ID.
* @return Device.
* @throws PushDeviceHandlerServerException Push Device Handler Server Exception.
*/
Optional getDeviceByUserId(String userId, int tenantId) throws PushDeviceHandlerServerException;
+ /**
+ * Get all devices registered for a user.
+ *
+ * @param userId User ID.
+ * @param tenantId Tenant ID.
+ * @return List of registered devices, empty list if none found.
+ * @throws PushDeviceHandlerServerException Push Device Handler Server Exception.
+ */
+ List getDevicesByUserId(String userId, int tenantId) throws PushDeviceHandlerServerException;
+
/**
* Get the public key of a device.
*
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImpl.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImpl.java
index 8080901..726790b 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImpl.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com).
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
@@ -21,14 +21,20 @@
import org.wso2.carbon.database.utils.jdbc.NamedPreparedStatement;
import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil;
import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants;
+import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerClientException;
+import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerException;
import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerServerException;
import org.wso2.carbon.identity.notification.push.device.handler.model.Device;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.SQLIntegrityConstraintViolationException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_ID_ALREADY_REGISTERED;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.SQLQueries.EDIT_DEVICE;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.SQLQueries.GET_DEVICE_BY_DEVICE_ID;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.SQLQueries.GET_DEVICE_BY_USER_ID;
@@ -42,7 +48,7 @@
public class DeviceDAOImpl implements DeviceDAO {
@Override
- public void registerDevice(Device device, int tenantId) throws PushDeviceHandlerServerException {
+ public void registerDevice(Device device, int tenantId) throws PushDeviceHandlerException {
Connection connection = IdentityDatabaseUtil.getDBConnection(true);
try (NamedPreparedStatement statement = new NamedPreparedStatement(connection, REGISTER_DEVICE)) {
@@ -59,7 +65,13 @@ public void registerDevice(Device device, int tenantId) throws PushDeviceHandler
IdentityDatabaseUtil.commitTransaction(connection);
} catch (SQLException e) {
IdentityDatabaseUtil.rollbackTransaction(connection);
- throw new PushDeviceHandlerServerException("Error occurred while registering the device.", e);
+ if (e instanceof SQLIntegrityConstraintViolationException) {
+ throw new PushDeviceHandlerClientException(
+ ERROR_CODE_DEVICE_ID_ALREADY_REGISTERED.getCode(),
+ ERROR_CODE_DEVICE_ID_ALREADY_REGISTERED.getMessage() + device.getDeviceId());
+ }
+ throw new PushDeviceHandlerServerException(
+ "Error occurred while registering the device.", e);
} finally {
IdentityDatabaseUtil.closeConnection(connection);
}
@@ -156,6 +168,39 @@ public Optional getDeviceByUserId(String userId, int tenantId) throws Pu
return Optional.ofNullable(device);
}
+ @Override
+ public List getDevicesByUserId(String userId, int tenantId)
+ throws PushDeviceHandlerServerException {
+
+ List devices = new ArrayList<>();
+ Connection connection = IdentityDatabaseUtil.getDBConnection(true);
+ try (NamedPreparedStatement statement =
+ new NamedPreparedStatement(connection, GET_DEVICE_BY_USER_ID)) {
+ statement.setString(PushDeviceHandlerConstants.ColumnNames.USER_ID, userId);
+ statement.setInt(PushDeviceHandlerConstants.ColumnNames.TENANT_ID, tenantId);
+ try (ResultSet resultSet = statement.executeQuery()) {
+ while (resultSet.next()) {
+ Device device = new Device();
+ device.setDeviceId(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.ID));
+ device.setUserId(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.USER_ID));
+ device.setDeviceName(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_NAME));
+ device.setDeviceModel(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_MODEL));
+ device.setDeviceToken(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_TOKEN));
+ device.setDeviceHandle(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_HANDLE));
+ device.setPublicKey(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.PUBLIC_KEY));
+ device.setProvider(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.PROVIDER));
+ devices.add(device);
+ }
+ }
+ } catch (SQLException e) {
+ throw new PushDeviceHandlerServerException(
+ "Error occurred while retrieving devices for user.", e);
+ } finally {
+ IdentityDatabaseUtil.closeConnection(connection);
+ }
+ return devices;
+ }
+
@Override
public Optional getPublicKey(String deviceId) throws PushDeviceHandlerServerException {
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImpl.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImpl.java
index 320092b..61433d9 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImpl.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImpl.java
@@ -18,17 +18,26 @@
package org.wso2.carbon.identity.notification.push.device.handler.impl;
+import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.annotation.bundle.Capability;
import org.wso2.carbon.identity.base.IdentityException;
+import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils;
+import org.wso2.carbon.identity.core.context.IdentityContext;
+import org.wso2.carbon.identity.core.context.model.Request;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
+import org.wso2.carbon.identity.event.IdentityEventConstants;
+import org.wso2.carbon.identity.event.IdentityEventException;
+import org.wso2.carbon.identity.event.event.Event;
+import org.wso2.carbon.identity.governance.service.notification.NotificationChannels;
import org.wso2.carbon.identity.notification.push.common.PushChallengeValidator;
import org.wso2.carbon.identity.notification.push.common.exception.PushTokenValidationException;
import org.wso2.carbon.identity.notification.push.device.handler.DeviceHandlerService;
import org.wso2.carbon.identity.notification.push.device.handler.DeviceRegistrationContextManager;
+import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants;
import org.wso2.carbon.identity.notification.push.device.handler.dao.DeviceDAO;
import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerClientException;
import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerException;
@@ -36,10 +45,13 @@
import org.wso2.carbon.identity.notification.push.device.handler.internal.PushDeviceHandlerDataHolder;
import org.wso2.carbon.identity.notification.push.device.handler.model.Device;
import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationContext;
+import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationNotificationChannelEnum;
+import org.wso2.carbon.identity.notification.push.device.handler.model.PushDeviceMgtConfigData;
import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationDiscoveryData;
import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationRequest;
import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationRequestProviderData;
import org.wso2.carbon.identity.notification.push.device.handler.utils.DeviceHandlerAuditLogger;
+import org.wso2.carbon.identity.notification.push.device.handler.utils.PushDeviceConfigManager;
import org.wso2.carbon.identity.notification.push.provider.PushProvider;
import org.wso2.carbon.identity.notification.push.provider.exception.PushProviderClientException;
import org.wso2.carbon.identity.notification.push.provider.exception.PushProviderException;
@@ -52,7 +64,10 @@
import org.wso2.carbon.identity.organization.management.service.util.OrganizationManagementUtil;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.api.UserStoreException;
+import org.wso2.carbon.user.core.UserCoreConstants;
import org.wso2.carbon.user.core.common.AbstractUserStoreManager;
+import org.wso2.carbon.user.core.common.User;
+import org.wso2.carbon.utils.DiagnosticLog;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;
import java.nio.charset.StandardCharsets;
@@ -64,13 +79,23 @@
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
+import static org.wso2.carbon.identity.event.handler.notification.NotificationConstants.EmailNotification.ARBITRARY_SEND_TO;
+import static org.wso2.carbon.identity.event.handler.notification.NotificationConstants.EmailNotification.EMAIL_TEMPLATE_TYPE;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_MIN_DEVICE_LIMIT_PER_USER;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_PUSH_PROVIDER;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.EmailNotificationConstants.PUSH_DEVICE_REGISTRATION_TEMPLATE;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_ALREADY_REGISTERED;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_NOT_FOUND;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_NOT_FOUND_FOR_USER_ID;
@@ -79,12 +104,27 @@
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_FAILED_TO_RESOLVE_PUSH_PROVIDER;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_EDIT_DEVICE_SCENARIO;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_SIGNATURE;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_MAX_DEVICE_LIMIT_REACHED;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_PUBLIC_KEY_NOT_FOUND;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_REGISTRATION_CONTEXT_ALREADY_USED;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_REGISTRATION_CONTEXT_NOT_FOUND;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_SIGNATURE_VERIFICATION_FAILED;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_TOKEN_CLAIM_VERIFICATION_FAILED;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.HASHING_ALGORITHM;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.LogConstants.ActionIDs.TRIGGER_DEVICE_REGISTRATION_EMAIL_NOTIFICATION;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.LogConstants.ActionIDs.TRIGGER_DEVICE_REGISTRATION_PUSH_NOTIFICATION;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.LogConstants.InputKeys.TENANT_DOMAIN;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.LogConstants.PUSH_DEVICE_HANDLER_SERVICE;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_HANDLE;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_ID;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_REGISTRATION_SCENARIO;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_TOKEN;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.IP_ADDRESS;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.NOTIFICATION_PROVIDER;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.NOTIFICATION_SCENARIO;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.PUSH_NOTIFICATION_CHANNEL;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.PUSH_NOTIFICATION_EVENT_NAME;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.REGISTRATION_TIME_FORMATTER_PATTERN;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.SIGNATURE_ALGORITHM;
import static org.wso2.carbon.identity.notification.sender.tenant.config.NotificationSenderManagementConstants.PUSH_PUBLISHER_TYPE;
@@ -104,6 +144,8 @@ public class DeviceHandlerServiceImpl implements DeviceHandlerService {
private DeviceDAO deviceDAO;
private DeviceRegistrationContextManager deviceRegistrationContextManager;
private static final DeviceHandlerAuditLogger AUDIT_LOGGER = new DeviceHandlerAuditLogger();
+ private static final DateTimeFormatter REGISTRATION_TIME_FORMATTER =
+ DateTimeFormatter.ofPattern(REGISTRATION_TIME_FORMATTER_PATTERN).withZone(ZoneOffset.UTC);
/**
* Constructor of DeviceHandlerServiceImpl.
@@ -145,6 +187,12 @@ public Device registerDevice(RegistrationRequest registrationRequest, String ten
String.format(ERROR_CODE_REGISTRATION_CONTEXT_ALREADY_USED.getMessage(), deviceId));
}
+ AUDIT_LOGGER.printAuditLog(
+ DeviceHandlerAuditLogger.Operation.REGISTER_DEVICE,
+ deviceId,
+ device.getUserId()
+ );
+
return device;
}
@@ -240,6 +288,15 @@ public Device getDeviceByUserId(String userId, String tenantDomain) throws PushD
}
}
+ @Override
+ public List getDevicesByUserId(String userId, String tenantDomain) throws PushDeviceHandlerException {
+
+ int tenantId = IdentityTenantUtil.getTenantId(tenantDomain);
+ List devices = deviceDAO.getDevicesByUserId(userId, tenantId);
+
+ return devices != null ? devices : Collections.emptyList();
+ }
+
@Override
public void editDevice(String deviceId, String path, String value) throws PushDeviceHandlerException {
@@ -367,39 +424,29 @@ private void handleSignatureVerification(RegistrationRequest registrationRequest
private Device handleDeviceRegistration(RegistrationRequest registrationRequest,
DeviceRegistrationContext context) throws PushDeviceHandlerException {
- String userId;
String username = context.getUsername();
String tenantDomain = context.getTenantDomain();
int tenantId = IdentityTenantUtil.getTenantId(tenantDomain);
- try {
- UserRealm userRealm = IdentityTenantUtil.getRealm(tenantDomain, username);
- AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) userRealm.getUserStoreManager();
- userId = userStoreManager.getUserIDFromUserName(MultitenantUtils.getTenantAwareUsername(username));
- if (StringUtils.isBlank(userId)) {
- String errorMessage = String.format(ERROR_CODE_FAILED_TO_GET_USER_ID.toString(), username);
- throw new PushDeviceHandlerServerException(errorMessage);
- }
- } catch (UserStoreException | IdentityException e) {
- String errorMessage = String.format(ERROR_CODE_FAILED_TO_GET_USER_ID.toString(), username);
- throw new PushDeviceHandlerServerException(errorMessage, e);
- }
- try {
- Device existingDevice = getDeviceByUserId(userId, tenantDomain);
- if (existingDevice != null) {
+ PushDeviceMgtConfigData tenantConfig = PushDeviceConfigManager.getPushDeviceConfig(tenantDomain);
+ User user = resolveUser(username, tenantDomain, tenantConfig);
+ String userId = user.getUserID();
+
+ List existingDevices = deviceDAO.getDevicesByUserId(userId, tenantId);
+
+ // if multiple device enrollment is not enabled, check if the user already has a registered device
+ if (!Boolean.TRUE.equals(tenantConfig.getEnableMultipleDeviceEnrollment())) {
+ if (!existingDevices.isEmpty()) {
throw new PushDeviceHandlerClientException(
ERROR_CODE_DEVICE_ALREADY_REGISTERED.getCode(),
ERROR_CODE_DEVICE_ALREADY_REGISTERED.toString());
}
- } catch (PushDeviceHandlerClientException e) {
- // This means there is no existing device registered for the user.
- if ((ERROR_CODE_DEVICE_NOT_FOUND_FOR_USER_ID.getCode()).equals(e.getErrorCode())) {
- if (LOG.isDebugEnabled()) {
- String message = String.format("No existing device registered for the user: %s", userId);
- LOG.debug(message);
- }
- } else {
- throw e;
+ } else {
+ int maxDeviceLimit = tenantConfig.getMaximumDeviceLimit() != null
+ ? tenantConfig.getMaximumDeviceLimit() : DEFAULT_MIN_DEVICE_LIMIT_PER_USER;
+ if (existingDevices.size() >= maxDeviceLimit) {
+ throw new PushDeviceHandlerClientException(ERROR_CODE_MAX_DEVICE_LIMIT_REACHED.getCode(),
+ String.format(ERROR_CODE_MAX_DEVICE_LIMIT_REACHED.getMessage(), userId));
}
}
@@ -421,15 +468,64 @@ private Device handleDeviceRegistration(RegistrationRequest registrationRequest,
try {
deviceDAO.registerDevice(device, tenantId);
context.setRegistered(true);
- } catch (PushDeviceHandlerServerException e) {
+ } catch (PushDeviceHandlerException e) {
String errorMessage = String.format(ERROR_CODE_DEVICE_REGISTRATION_FAILED.toString(),
registrationRequest.getDeviceId());
throw new PushDeviceHandlerServerException(errorMessage, e);
}
+ // Trigger notifications about the new device registration.
+ if (Boolean.TRUE.equals(tenantConfig.getEnableDeviceRegistrationNotifications())) {
+ Set channels =
+ tenantConfig.getDeviceRegistrationNotificationChannels();
+ if (channels != null && channels.contains(DeviceRegistrationNotificationChannelEnum.EMAIL)) {
+ triggerEmailNotification(device, tenantDomain, user);
+ }
+ if (channels != null && channels.contains(DeviceRegistrationNotificationChannelEnum.PUSH_NOTIFICATION)) {
+ triggerPushNotification(device, existingDevices, user, tenantDomain);
+ }
+ }
+
return device;
}
+ /**
+ * Resolve the user owning the device registration, along with the claims
+ * required for the device registration notifications.
+ *
+ * @param username Username from the device registration context.
+ * @param tenantDomain Tenant domain of the user.
+ * @return Resolved user; never {@code null}.
+ * @throws PushDeviceHandlerServerException If the user cannot be resolved.
+ */
+ private User resolveUser(String username, String tenantDomain, PushDeviceMgtConfigData tenantConfig)
+ throws PushDeviceHandlerServerException {
+
+ try {
+ UserRealm userRealm = IdentityTenantUtil.getRealm(tenantDomain, username);
+ AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) userRealm.getUserStoreManager();
+ String tenantAwareUsername = MultitenantUtils.getTenantAwareUsername(username);
+ String userId = userStoreManager.getUserIDFromUserName(tenantAwareUsername);
+ if (StringUtils.isBlank(userId)) {
+ String errorMessage = String.format(ERROR_CODE_FAILED_TO_GET_USER_ID.toString(), username);
+ throw new PushDeviceHandlerServerException(errorMessage);
+ }
+
+ User user;
+ if (Boolean.TRUE.equals(tenantConfig.getEnableDeviceRegistrationNotifications())) {
+ String[] requestedClaims = new String[] {NotificationChannels.EMAIL_CHANNEL.getClaimUri()};
+ user = userStoreManager.getUserWithID(userId, requestedClaims, UserCoreConstants.DEFAULT_PROFILE);
+ } else {
+ user = new User(userId, tenantAwareUsername, null);
+ }
+
+ return user;
+ } catch (UserStoreException | IdentityException e) {
+ String errorMessage = String.format(ERROR_CODE_FAILED_TO_GET_USER_ID.toString(), username);
+ throw new PushDeviceHandlerServerException(errorMessage, e);
+ }
+ }
+
/**
* Handle the device registration for the push notification providers.
*
@@ -594,7 +690,7 @@ private String getPushProviderName(RegistrationRequestProviderData providerData)
String pushProvider = pushSenders.get(0).getProvider();
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Only one push sender is available: %s. " +
- "Using it as the push provider.", pushProvider));
+ "Using it as the push provider.", pushProvider));
}
return pushProvider;
}
@@ -609,7 +705,7 @@ private String getPushProviderName(RegistrationRequestProviderData providerData)
LOG.debug("Error occurred while retrieving the default push notification provider", e);
throw new PushDeviceHandlerServerException(
"Error occurred while retrieving the default push notification provider.", e);
- }
+ }
}
/**
@@ -673,4 +769,203 @@ public static PushSenderData buildPushSenderData(PushSenderDTO pushSenderDTO) {
pushSenderData.setProviderId(pushSenderDTO.getProviderId());
return pushSenderData;
}
+
+ /**
+ * Trigger an email notification informing the user that a new device has
+ * been registered for their account.
+ *
+ * @param device Newly registered device.
+ * @param tenantDomain Tenant domain of the user.
+ * @param user Resolved user, used to obtain the username and email claim.
+ */
+ private void triggerEmailNotification(Device device, String tenantDomain, User user) {
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format(
+ "Sending device registration email notification for the device ID: %s.",
+ device.getDeviceId()));
+ }
+
+ String userId = user.getUserID();
+ String email = getEmailAddress(user);
+ if (StringUtils.isBlank(email)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Email address not found for userId: " + userId
+ + ". Skipping device registration email notification.");
+ }
+ return;
+ }
+
+ HashMap properties = new HashMap<>();
+
+ properties.put(IdentityEventConstants.EventProperty.USER_NAME, user.getUsername());
+ properties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain);
+ properties.put(IdentityEventConstants.EventProperty.USER_STORE_DOMAIN, user.getUserStoreDomain());
+
+ // Explicit recipient so the handler does not need to re-resolve claims.
+ properties.put(ARBITRARY_SEND_TO, email);
+
+ // Tell the notification handler which template to render.
+ properties.put(EMAIL_TEMPLATE_TYPE, PUSH_DEVICE_REGISTRATION_TEMPLATE);
+
+ // Custom placeholders the template can reference.
+ properties.put(PushDeviceHandlerConstants.EmailNotificationConstants.DEVICE_NAME_PLACEHOLDER,
+ device.getDeviceName());
+ properties.put(PushDeviceHandlerConstants.EmailNotificationConstants.DEVICE_MODEL_PLACEHOLDER,
+ device.getDeviceModel());
+ properties.put(PushDeviceHandlerConstants.EmailNotificationConstants.REGISTRATION_TIME_PLACEHOLDER,
+ formatRegistrationTime(System.currentTimeMillis()));
+ String ipAddress = resolveClientIpAddress();
+ if (StringUtils.isNotBlank(ipAddress)) {
+ properties.put(PushDeviceHandlerConstants.EmailNotificationConstants.IP_ADDRESS_PLACEHOLDER, ipAddress);
+ }
+
+ try {
+ Event event = new Event(IdentityEventConstants.Event.TRIGGER_NOTIFICATION, properties);
+ PushDeviceHandlerDataHolder.getInstance().getIdentityEventService().handleEvent(event);
+ } catch (IdentityEventException e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Error while triggering device registration email "
+ + "notification for userId: " + userId, e);
+ }
+
+ if (LoggerUtils.isDiagnosticLogsEnabled()) {
+ DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder(
+ PUSH_DEVICE_HANDLER_SERVICE, TRIGGER_DEVICE_REGISTRATION_EMAIL_NOTIFICATION);
+ diagnosticLogBuilder
+ .resultMessage("Error while triggering the device registration email notification.")
+ .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION)
+ .resultStatus(DiagnosticLog.ResultStatus.FAILED)
+ .inputParam(PushDeviceHandlerConstants.LogConstants.InputKeys.DEVICE_ID, device.getDeviceId())
+ .inputParam(PushDeviceHandlerConstants.LogConstants.InputKeys.USER_ID, userId)
+ .inputParam(TENANT_DOMAIN, tenantDomain);
+ LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder);
+ }
+ }
+ }
+
+ /**
+ * Get the email address from the resolved user.
+ *
+ * @param user Resolved user.
+ * @return Email address, or {@code null} if not available.
+ */
+ private String getEmailAddress(User user) {
+
+ Map userAttributes = user.getAttributes();
+ if (MapUtils.isEmpty(userAttributes)) {
+ return null;
+ }
+ return userAttributes.get(NotificationChannels.EMAIL_CHANNEL.getClaimUri());
+ }
+
+ /**
+ * Trigger push notifications to the user's previously registered devices to
+ * notify that a new device has been registered for the account.
+ *
+ * @param registeredDevice Newly registered device.
+ * @param existingDevices Previously registered devices of the user.
+ * @param user Resolved user who owns the devices.
+ * @param tenantDomain Tenant domain of the user.
+ */
+ private void triggerPushNotification(Device registeredDevice, List existingDevices, User user,
+ String tenantDomain) {
+
+ if (existingDevices == null || existingDevices.isEmpty()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("No previously registered devices found to notify about "
+ + "the registration of device ID: %s.",
+ registeredDevice.getDeviceId()));
+ }
+ return;
+ }
+
+ String registrationTime = formatRegistrationTime(System.currentTimeMillis());
+ String ipAddress = resolveClientIpAddress();
+
+ HashMap properties = new HashMap<>();
+
+ // Standard identity event properties expected by the handler.
+ properties.put(IdentityEventConstants.EventProperty.USER_ID, registeredDevice.getUserId());
+ properties.put(IdentityEventConstants.EventProperty.USER_NAME, user.getUsername());
+ properties.put(IdentityEventConstants.EventProperty.USER_STORE_DOMAIN, user.getUserStoreDomain());
+ properties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain);
+ properties.put(IdentityEventConstants.EventProperty.NOTIFICATION_CHANNEL, PUSH_NOTIFICATION_CHANNEL);
+ properties.put(NOTIFICATION_SCENARIO, DEVICE_REGISTRATION_SCENARIO);
+
+ // Details of the newly registered device rendered in the notification.
+ properties.put(PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_NAME_PLACEHOLDER,
+ StringUtils.defaultString(registeredDevice.getDeviceName()));
+ properties.put(PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_MODEL_PLACEHOLDER,
+ StringUtils.defaultString(registeredDevice.getDeviceModel()));
+ properties.put(PushDeviceHandlerConstants.PushNotificationConstants.REGISTRATION_TIME_PLACEHOLDER,
+ registrationTime);
+ if (StringUtils.isNotBlank(ipAddress)) {
+ properties.put(IP_ADDRESS, ipAddress);
+ }
+
+ for (Device device : existingDevices) {
+
+ // The notification is delivered to the previously registered device.
+ properties.put(NOTIFICATION_PROVIDER, device.getProvider());
+ properties.put(DEVICE_TOKEN, device.getDeviceToken());
+ properties.put(DEVICE_ID, device.getDeviceId());
+ properties.put(DEVICE_HANDLE, device.getDeviceHandle());
+
+ try {
+ Event event = new Event(PUSH_NOTIFICATION_EVENT_NAME, properties);
+ PushDeviceHandlerDataHolder.getInstance().getIdentityEventService().handleEvent(event);
+ } catch (IdentityEventException e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format(
+ "Error while triggering the device registration push notification to the device ID: %s.",
+ device.getDeviceId()), e);
+ }
+
+ if (LoggerUtils.isDiagnosticLogsEnabled()) {
+ DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder(
+ PUSH_DEVICE_HANDLER_SERVICE, TRIGGER_DEVICE_REGISTRATION_PUSH_NOTIFICATION);
+ diagnosticLogBuilder
+ .resultMessage("Error while triggering the device registration push notification.")
+ .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION)
+ .resultStatus(DiagnosticLog.ResultStatus.FAILED)
+ .inputParam(PushDeviceHandlerConstants.LogConstants.InputKeys.DEVICE_ID,
+ device.getDeviceId())
+ .inputParam(PushDeviceHandlerConstants.LogConstants.InputKeys.USER_ID,
+ registeredDevice.getUserId())
+ .inputParam(TENANT_DOMAIN, tenantDomain);
+ LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder);
+ }
+ }
+ }
+ }
+
+ /**
+ * Resolve the client IP address of the current request.
+ *
+ * @return Client IP address, or {@code null} if it cannot be resolved.
+ */
+ private String resolveClientIpAddress() {
+
+ Request request = IdentityContext.getThreadLocalIdentityContext().getRequest();
+ if (request == null || StringUtils.isBlank(request.getIpAddress())) {
+ return null;
+ }
+
+ return request.getIpAddress();
+ }
+
+ /**
+ * Format a registration timestamp consistently across notification channels.
+ *
+ * @param time Registration time.
+ * @return Human-readable UTC timestamp.
+ */
+ private String formatRegistrationTime(long time) {
+
+ Instant instant = Instant.ofEpochMilli(time);
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(REGISTRATION_TIME_FORMATTER_PATTERN)
+ .withZone(ZoneOffset.UTC);
+ return formatter.format(instant);
+ }
}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImpl.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImpl.java
new file mode 100644
index 0000000..a10e1ae
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.impl;
+
+
+import org.wso2.carbon.identity.configuration.mgt.core.DefaultConfigResolver;
+import org.wso2.carbon.identity.configuration.mgt.core.model.Resource;
+import org.wso2.carbon.identity.configuration.mgt.core.model.ResourceIdentifier;
+import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants;
+import org.wso2.carbon.identity.notification.push.device.handler.utils.PushDeviceConfigManager;
+
+/**
+ * Default config resolver for push device management configurations. Serves the default
+ * device management configs when no resource is available in the configuration store.
+ */
+public class PushDefaultConfigResolverImpl implements DefaultConfigResolver {
+
+ @Override
+ public ResourceIdentifier getResourceIdentifier() {
+
+ return new ResourceIdentifier(
+ PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE,
+ PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME);
+ }
+
+ @Override
+ public Resource getDefaultConfigs(
+ String resourceTypeName, String resourceName) {
+
+ return PushDeviceConfigManager.getDefaultPushDeviceMgtConfigs();
+ }
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolder.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolder.java
index 5c7a206..c93c74a 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolder.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolder.java
@@ -18,6 +18,8 @@
package org.wso2.carbon.identity.notification.push.device.handler.internal;
+import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager;
+import org.wso2.carbon.identity.event.services.IdentityEventService;
import org.wso2.carbon.identity.notification.push.provider.PushProvider;
import org.wso2.carbon.identity.notification.sender.tenant.config.NotificationSenderManagementService;
import org.wso2.carbon.identity.organization.management.service.OrganizationManager;
@@ -36,6 +38,8 @@ public class PushDeviceHandlerDataHolder {
private RealmService realmService;
private OrganizationManager organizationManager;
private NotificationSenderManagementService notificationSenderManagementService;
+ private ConfigurationManager configurationManager;
+ private IdentityEventService identityEventService;
private PushDeviceHandlerDataHolder() {
@@ -138,4 +142,42 @@ public PushProvider getPushProvider(String providerName) {
return pushNotificationProviders.get(providerName);
}
+
+ /**
+ * Get the ConfigurationManager.
+ *
+ * @return ConfigurationManager instance.
+ */
+ public ConfigurationManager getConfigurationManager() {
+
+ return configurationManager;
+ }
+
+ /**
+ * Set the ConfigurationManager.
+ *
+ * @param configurationManager ConfigurationManager instance.
+ */
+ public void setConfigurationManager(ConfigurationManager configurationManager) {
+
+ this.configurationManager = configurationManager;
+ }
+
+ /**
+ * Get the IdentityEventService.
+ * @return
+ */
+ public IdentityEventService getIdentityEventService() {
+
+ return identityEventService;
+ }
+
+ /**
+ * Set the identityEventService.
+ * @param identityEventService
+ */
+ public void setIdentityEventService(IdentityEventService identityEventService) {
+
+ this.identityEventService = identityEventService;
+ }
}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponent.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponent.java
index 6844fd6..8bcd4fd 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponent.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponent.java
@@ -27,12 +27,16 @@
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
+import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager;
+import org.wso2.carbon.identity.configuration.mgt.core.DefaultConfigResolver;
+import org.wso2.carbon.identity.event.services.IdentityEventService;
import org.wso2.carbon.identity.notification.push.device.handler.DeviceHandlerService;
import org.wso2.carbon.identity.notification.push.device.handler.DeviceRegistrationContextManager;
import org.wso2.carbon.identity.notification.push.device.handler.dao.DeviceDAO;
import org.wso2.carbon.identity.notification.push.device.handler.dao.DeviceDAOImpl;
import org.wso2.carbon.identity.notification.push.device.handler.impl.DeviceHandlerServiceImpl;
import org.wso2.carbon.identity.notification.push.device.handler.impl.DeviceRegistrationContextManagerImpl;
+import org.wso2.carbon.identity.notification.push.device.handler.impl.PushDefaultConfigResolverImpl;
import org.wso2.carbon.identity.notification.push.provider.PushProvider;
import org.wso2.carbon.identity.notification.sender.tenant.config.NotificationSenderManagementService;
import org.wso2.carbon.identity.organization.management.service.OrganizationManager;
@@ -59,6 +63,8 @@ protected void activate(ComponentContext context) {
new DeviceHandlerServiceImpl(deviceRegistrationContextManager, deviceDAO);
context.getBundleContext().registerService(
DeviceHandlerService.class.getName(), deviceHandlerService, null);
+ context.getBundleContext().registerService(
+ DefaultConfigResolver.class.getName(), new PushDefaultConfigResolverImpl(), null);
} catch (Throwable e) {
LOG.error("Error occurred while activating Push Device Handler Service Component", e);
}
@@ -131,4 +137,36 @@ protected void unsetProvider(PushProvider provider) {
PushDeviceHandlerDataHolder.getInstance().removePushProvider(provider.getName());
}
+
+ @Reference(
+ name = "configuration.manager",
+ service = ConfigurationManager.class,
+ cardinality = ReferenceCardinality.MANDATORY,
+ policy = ReferencePolicy.DYNAMIC,
+ unbind = "unsetConfigurationManager")
+ protected void setConfigurationManager(ConfigurationManager configurationManager) {
+
+ PushDeviceHandlerDataHolder.getInstance().setConfigurationManager(configurationManager);
+ }
+
+ protected void unsetConfigurationManager(ConfigurationManager configurationManager) {
+
+ PushDeviceHandlerDataHolder.getInstance().setConfigurationManager(null);
+ }
+
+ @Reference(
+ name = "EventMgtService",
+ service = org.wso2.carbon.identity.event.services.IdentityEventService.class,
+ cardinality = ReferenceCardinality.MANDATORY,
+ policy = ReferencePolicy.DYNAMIC,
+ unbind = "unsetIdentityEventService")
+ protected void setIdentityEventService(IdentityEventService eventService) {
+
+ PushDeviceHandlerDataHolder.getInstance().setIdentityEventService(eventService);
+ }
+
+ protected void unsetIdentityEventService(IdentityEventService eventService) {
+
+ PushDeviceHandlerDataHolder.getInstance().setIdentityEventService(null);
+ }
}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/DeviceRegistrationNotificationChannelEnum.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/DeviceRegistrationNotificationChannelEnum.java
new file mode 100644
index 0000000..ab37dec
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/DeviceRegistrationNotificationChannelEnum.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.model;
+
+/**
+ * Channel used to notify the user when a device registration completes.
+ */
+public enum DeviceRegistrationNotificationChannelEnum {
+
+ EMAIL,
+ PUSH_NOTIFICATION
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/PushDeviceMgtConfigData.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/PushDeviceMgtConfigData.java
new file mode 100644
index 0000000..42e8d08
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/PushDeviceMgtConfigData.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2023-2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.model;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+
+/**
+ * This class represents the push device management configuration.
+ */
+public class PushDeviceMgtConfigData {
+
+ private Boolean enableMultipleDeviceEnrollment;
+ private Integer maximumDeviceLimit;
+ private Boolean enableDeviceRegistrationNotifications;
+ private Set deviceRegistrationNotificationChannelEnums;
+
+ /**
+ * Get whether multiple device enrollment is enabled.
+ *
+ * @return Whether multiple device enrollment is enabled.
+ */
+ public Boolean getEnableMultipleDeviceEnrollment() {
+
+ return enableMultipleDeviceEnrollment;
+ }
+
+ /**
+ * Set whether multiple device enrollment is enabled.
+ *
+ * @param enableMultipleDeviceEnrollment Whether multiple device enrollment
+ * is enabled.
+ */
+ public void setEnableMultipleDeviceEnrollment(Boolean enableMultipleDeviceEnrollment) {
+
+ this.enableMultipleDeviceEnrollment = enableMultipleDeviceEnrollment;
+ }
+
+ /**
+ * Get the maximum device limit.
+ *
+ * @return Maximum device limit.
+ */
+ public Integer getMaximumDeviceLimit() {
+
+ return maximumDeviceLimit;
+ }
+
+ /**
+ * Set the maximum device limit.
+ *
+ * @param maximumDeviceLimit Maximum device limit.
+ */
+ public void setMaximumDeviceLimit(Integer maximumDeviceLimit) {
+
+ this.maximumDeviceLimit = maximumDeviceLimit;
+ }
+
+ /**
+ * Get whether device registration notifications are enabled.
+ *
+ * @return Whether device registration notifications are enabled.
+ */
+ public Boolean getEnableDeviceRegistrationNotifications() {
+
+ return enableDeviceRegistrationNotifications;
+ }
+
+ /**
+ * Set whether device registration notifications are enabled.
+ *
+ * @param enableDeviceRegistrationNotifications Whether device registration
+ * notifications are enabled.
+ */
+ public void setEnableDeviceRegistrationNotifications(Boolean enableDeviceRegistrationNotifications) {
+
+ this.enableDeviceRegistrationNotifications = enableDeviceRegistrationNotifications;
+ }
+
+ /**
+ * Get the channels used to deliver device registration notifications.
+ *
+ * @return Unmodifiable view of the configured channels, or {@code null}
+ * when the channels have not been set.
+ */
+ public Set getDeviceRegistrationNotificationChannels() {
+
+ if (deviceRegistrationNotificationChannelEnums == null) {
+ return null;
+ }
+ return Collections.unmodifiableSet(deviceRegistrationNotificationChannelEnums);
+ }
+
+ /**
+ * Set the channels used to deliver device registration notifications.
+ *
+ * @param deviceRegistrationNotificationChannelEnums Notification channels.
+ */
+ public void setDeviceRegistrationNotificationChannels(
+ Set deviceRegistrationNotificationChannelEnums) {
+
+ if (deviceRegistrationNotificationChannelEnums == null) {
+ this.deviceRegistrationNotificationChannelEnums = null;
+ return;
+ }
+ this.deviceRegistrationNotificationChannelEnums = deviceRegistrationNotificationChannelEnums.isEmpty()
+ ? EnumSet.noneOf(DeviceRegistrationNotificationChannelEnum.class)
+ : EnumSet.copyOf(deviceRegistrationNotificationChannelEnums);
+ }
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLogger.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLogger.java
index b3b2d33..602dc3a 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLogger.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLogger.java
@@ -45,7 +45,7 @@ public class DeviceHandlerAuditLogger {
*/
public void printAuditLog(Operation operation, String deviceId, String userId) {
- JSONObject data = createAuditLogEntry(userId);
+ JSONObject data = createAuditLogEntry(userId, operation, deviceId);
buildAuditLog(operation, deviceId, data);
}
@@ -72,11 +72,18 @@ private void buildAuditLog(Operation operation, String targetId, JSONObject data
*
* @return Audit log data.
*/
- private JSONObject createAuditLogEntry(String userId) {
+ private JSONObject createAuditLogEntry(String userId, Operation operation, String deviceId) {
JSONObject data = new JSONObject();
data.put(LogConstants.END_USER_ID, userId != null ? userId : JSONObject.NULL);
- data.put(LogConstants.UNREGISTERED_AT, System.currentTimeMillis());
+ if (operation.equals(Operation.REGISTER_DEVICE)) {
+ data.put(LogConstants.REGISTERED_AT, System.currentTimeMillis());
+ } else if (operation.equals(Operation.UPDATE_DEVICE_MGT_CONFIG)) {
+ data.put(LogConstants.UPDATED_AT, System.currentTimeMillis());
+ } else {
+ data.put(LogConstants.UNREGISTERED_AT, System.currentTimeMillis());
+ }
+ data.put(LogConstants.DEVICE_ID, deviceId != null ? deviceId : JSONObject.NULL);
return data;
}
@@ -128,7 +135,9 @@ private String getInitiatorId() {
*/
public enum Operation {
- UNREGISTER_DEVICE("Unregister-Push-Auth-Device");
+ UNREGISTER_DEVICE("Unregister-Push-Auth-Device"),
+ REGISTER_DEVICE("Register-Push-Auth-Device"),
+ UPDATE_DEVICE_MGT_CONFIG("Update-Push-Auth-Device-Mgt-Config");
private final String logAction;
@@ -150,6 +159,9 @@ private static class LogConstants {
public static final String TARGET_TYPE_FIELD = "Push-Auth-Device";
public static final String END_USER_ID = "UserId";
+ public static final String DEVICE_ID = "DeviceId";
public static final String UNREGISTERED_AT = "UnregisteredAt";
+ public static final String REGISTERED_AT = "RegisteredAt";
+ public static final String UPDATED_AT = "UpdatedAt";
}
}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManager.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManager.java
new file mode 100644
index 0000000..b1bb946
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManager.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.utils;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.wso2.carbon.context.CarbonContext;
+import org.wso2.carbon.identity.base.IdentityException;
+import org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants;
+import org.wso2.carbon.identity.configuration.mgt.core.exception.ConfigurationManagementException;
+import org.wso2.carbon.identity.configuration.mgt.core.model.Attribute;
+import org.wso2.carbon.identity.configuration.mgt.core.model.Resource;
+import org.wso2.carbon.identity.configuration.mgt.core.model.ResourceTypeAdd;
+import org.wso2.carbon.identity.core.util.IdentityUtil;
+import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants;
+import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerClientException;
+import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerException;
+import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerServerException;
+import org.wso2.carbon.identity.notification.push.device.handler.internal.PushDeviceHandlerDataHolder;
+import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationNotificationChannelEnum;
+import org.wso2.carbon.identity.notification.push.device.handler.model.PushDeviceMgtConfigData;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_DOES_NOT_EXISTS;
+import static org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_MAX_DEVICE_LIMIT_PER_USER;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_MIN_DEVICE_LIMIT_PER_USER;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.MAX_DEVICE_LIMIT_PER_USER;
+
+/**
+ * Push device config manager.
+ */
+public class PushDeviceConfigManager {
+
+ private static final Log LOG = LogFactory.getLog(PushDeviceConfigManager.class);
+ private static final DeviceHandlerAuditLogger AUDIT_LOGGER = new DeviceHandlerAuditLogger();
+
+ /**
+ * Get the maximum device limit per user.
+ *
+ * @return maximum device limit per user.
+ */
+ private static int getMaxDeviceLimitPerUser() {
+
+ String configValue = IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER);
+ if (StringUtils.isNotBlank(configValue)) {
+ try {
+ return Integer.parseInt(configValue.trim());
+ } catch (NumberFormatException e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Invalid value configured for '" + MAX_DEVICE_LIMIT_PER_USER + "': "
+ + configValue + ". Using default: " + DEFAULT_MAX_DEVICE_LIMIT_PER_USER);
+ }
+ }
+ }
+ return DEFAULT_MAX_DEVICE_LIMIT_PER_USER;
+ }
+
+ /**
+ * Get the push device configuration resource for the given tenant.
+ *
+ * @param tenantDomain Tenant domain.
+ * @return Push device configuration resource.
+ * @throws PushDeviceHandlerServerException If an error occurs while retrieving the config.
+ */
+ public static PushDeviceMgtConfigData getPushDeviceConfig(String tenantDomain)
+ throws PushDeviceHandlerServerException {
+
+ Resource resource = getResource(tenantDomain, true);
+ if (resource == null || resource.getAttributes() == null || resource.getAttributes().isEmpty()) {
+ resource = getDefaultPushDeviceMgtConfigs();
+ }
+
+ return buildDTOFromResource(resource);
+ }
+
+ /**
+ * Retrieve the configuration resource for the given tenant domain.
+ *
+ * @param tenantDomain Tenant domain.
+ * @param resolve Whether to resolve the resource from the parent organization hierarchy.
+ * @return Configuration resource, or null if not found.
+ * @throws PushDeviceHandlerServerException If an error occurs while retrieving the config.
+ */
+ private static Resource getResource(String tenantDomain, boolean resolve) throws PushDeviceHandlerServerException {
+
+ Resource resource = null;
+
+ try {
+ resource = PushDeviceHandlerDataHolder.getInstance().getConfigurationManager()
+ .getResource(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE,
+ PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME, resolve);
+ } catch (ConfigurationManagementException e) {
+ if (!ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode().equals(e.getErrorCode())
+ && !ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode().equals(e.getErrorCode())) {
+ throw new PushDeviceHandlerServerException(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getCode(),
+ ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getMessage(), e);
+ }
+ }
+ return resource;
+ }
+
+ /**
+ * Update the push device configuration for the given tenant.
+ *
+ * @param config Push device management configuration to persist.
+ * @param tenantDomain Tenant domain.
+ * @throws PushDeviceHandlerException If an error occurs while updating the config.
+ */
+ public static PushDeviceMgtConfigData updatePushDeviceConfig(PushDeviceMgtConfigData config, String tenantDomain)
+ throws PushDeviceHandlerException {
+
+ boolean multipleDeviceEnrollment = Boolean.TRUE.equals(config.getEnableMultipleDeviceEnrollment());
+ Integer maximumDeviceLimit = config.getMaximumDeviceLimit();
+
+ if (multipleDeviceEnrollment) {
+ if (maximumDeviceLimit == null || maximumDeviceLimit < 1) {
+ throw new PushDeviceHandlerClientException(ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE.getCode(),
+ ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE.getMessage());
+ }
+ int upperBound = getMaxDeviceLimitPerUser();
+ if (maximumDeviceLimit > upperBound) {
+ throw new PushDeviceHandlerClientException(ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS.getCode(),
+ ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS.getMessage()
+ + String.format(" Configured upper bound: %s", upperBound));
+ }
+ } else if (maximumDeviceLimit != null && maximumDeviceLimit > 1) {
+ throw new PushDeviceHandlerClientException(ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG.getCode(),
+ ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG.getMessage());
+ }
+
+ Resource resource = buildResourceFromConfig(config);
+ Resource existingResource = getResource(tenantDomain, false);
+
+ if (existingResource != null && existingResource.getAttributes() != null
+ && !existingResource.getAttributes().isEmpty()) {
+ replaceResource(resource);
+ } else {
+ addResource(resource);
+ }
+
+ AUDIT_LOGGER.printAuditLog(
+ DeviceHandlerAuditLogger.Operation.UPDATE_DEVICE_MGT_CONFIG,
+ tenantDomain,
+ CarbonContext.getThreadLocalCarbonContext().getUserId()
+ );
+
+ return config;
+ }
+
+ /**
+ * Replace an existing configuration resource for the given tenant domain.
+ *
+ * @param resource Resource to replace.
+ * @throws PushDeviceHandlerException If an error occurs while replacing the resource.
+ */
+ private static void replaceResource(Resource resource) throws PushDeviceHandlerServerException {
+
+ try {
+ PushDeviceHandlerDataHolder.getInstance().getConfigurationManager()
+ .replaceResource(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, resource);
+ } catch (ConfigurationManagementException e) {
+ throw new PushDeviceHandlerServerException(ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG.getCode(),
+ ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG.getMessage(), e);
+ }
+ }
+
+ /**
+ * Build a configuration resource from the given DTO.
+ *
+ * @param config Push device management configuration.
+ * @return Configuration resource.
+ */
+ private static Resource buildResourceFromConfig(PushDeviceMgtConfigData config) {
+
+ Resource resource = new Resource();
+ resource.setResourceName(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME);
+
+ List attributes = new ArrayList<>();
+ attributes.add(new Attribute(PushDeviceHandlerConstants.ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT,
+ String.valueOf(Boolean.TRUE.equals(config.getEnableMultipleDeviceEnrollment()))));
+
+ int maximumDeviceLimit = config.getMaximumDeviceLimit() != null ? config.getMaximumDeviceLimit() :
+ DEFAULT_MIN_DEVICE_LIMIT_PER_USER;
+ attributes.add(new Attribute(PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT,
+ String.valueOf(maximumDeviceLimit)));
+ attributes.add(new Attribute(PushDeviceHandlerConstants.ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS,
+ String.valueOf(Boolean.TRUE.equals(config.getEnableDeviceRegistrationNotifications()))));
+ attributes.add(new Attribute(PushDeviceHandlerConstants.ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS,
+ serializeChannels(config.getDeviceRegistrationNotificationChannels())));
+
+ resource.setAttributes(attributes);
+ return resource;
+ }
+
+ /**
+ * Build a default configuration resource.
+ *
+ * @return Default configuration resource.
+ */
+ public static Resource getDefaultPushDeviceMgtConfigs() {
+
+ Resource resource = new Resource(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME,
+ PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE);
+
+ List attributeList = new ArrayList<>();
+ attributeList.add(new Attribute(PushDeviceHandlerConstants.ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT, "false"));
+ attributeList.add(new Attribute(PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT, "2"));
+ attributeList.add(
+ new Attribute(PushDeviceHandlerConstants.ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS, "false"));
+ attributeList.add(
+ new Attribute(PushDeviceHandlerConstants.ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS,
+ DeviceRegistrationNotificationChannelEnum.EMAIL.name()));
+
+ resource.setAttributes(attributeList);
+ return resource;
+ }
+
+ /**
+ * Add a configuration resource for the given tenant domain.
+ *
+ * @param resource Resource to add.
+ * @return Added resource.
+ * @throws IdentityException If an error occurs while adding the resource.
+ */
+ private static Resource addResource(Resource resource) throws PushDeviceHandlerServerException {
+
+ try {
+ return PushDeviceHandlerDataHolder.getInstance().getConfigurationManager()
+ .addResource(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, resource);
+ } catch (ConfigurationManagementException e) {
+ if (ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode()
+ .equals(e.getErrorCode())) {
+
+ /* If the insert is failing due to the relevant resource-type is not existing in the database,
+ create the resource-type and retry the configuration addition. */
+ createResourceType();
+ return addResource(resource);
+ }
+ throw new PushDeviceHandlerServerException(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getCode(),
+ ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage(), e);
+ }
+ }
+
+ /**
+ * Create the resource type for push authentication settings.
+ *
+ * @throws IdentityException If an error occurs while creating the resource
+ * type.
+ */
+ private static void createResourceType() throws PushDeviceHandlerServerException {
+
+ try {
+ ResourceTypeAdd resourceTypeAdd = new ResourceTypeAdd();
+ resourceTypeAdd.setName(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE);
+ resourceTypeAdd.setDescription("Resource Type for "
+ + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE);
+ PushDeviceHandlerDataHolder.getInstance().getConfigurationManager().addResourceType(resourceTypeAdd);
+ } catch (ConfigurationManagementException e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Error while creating resource type: "
+ + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, e);
+ }
+ throw new PushDeviceHandlerServerException(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getCode(),
+ ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage(), e);
+ }
+ }
+
+ /**
+ * Build a configuration DTO from the given resource.
+ *
+ * @param resource Configuration resource.
+ * @return Configuration resource or default if resource is empty.
+ */
+ private static PushDeviceMgtConfigData buildDTOFromResource(Resource resource) {
+
+ PushDeviceMgtConfigData config = new PushDeviceMgtConfigData();
+ resource.getAttributes().forEach(attribute -> {
+ if (PushDeviceHandlerConstants.ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT.equals(attribute.getKey())) {
+ config.setEnableMultipleDeviceEnrollment(Boolean.parseBoolean(attribute.getValue()));
+ } else if (PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT.equals(attribute.getKey())) {
+ config.setMaximumDeviceLimit(parseMaxDeviceLimit(attribute.getValue()));
+ } else if (PushDeviceHandlerConstants.ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS
+ .equals(attribute.getKey())) {
+ config.setEnableDeviceRegistrationNotifications(Boolean.parseBoolean(attribute.getValue()));
+ } else if (PushDeviceHandlerConstants.ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS
+ .equals(attribute.getKey())) {
+ config.setDeviceRegistrationNotificationChannels(parseNotificationChannels(attribute.getValue()));
+ }
+ });
+
+ return config;
+ }
+
+ /**
+ * Parse a stored maximum device limit value into an integer.
+ *
+ * @param value Stored attribute value.
+ * @return Parsed maximum device limit, defaulting to 1.
+ */
+ private static int parseMaxDeviceLimit(String value) {
+
+ if (StringUtils.isNotBlank(value)) {
+ try {
+ return Integer.parseInt(value.trim());
+ } catch (NumberFormatException e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Invalid value stored for '" + PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT
+ + "': " + value + ". Using default:" + DEFAULT_MIN_DEVICE_LIMIT_PER_USER + ".", e);
+ }
+ }
+ }
+ return DEFAULT_MIN_DEVICE_LIMIT_PER_USER;
+ }
+
+ /**
+ * Serialize the configured notification channels into a comma-separated string.
+ *
+ * @param channels Channels to serialize.
+ * @return Comma-separated channel names, or an empty string when none are set.
+ */
+ private static String serializeChannels(Set channels) {
+
+ if (channels == null || channels.isEmpty()) {
+ return "";
+ }
+ return channels.stream()
+ .map(Enum::name)
+ .collect(Collectors.joining(PushDeviceHandlerConstants.NOTIFICATION_CHANNELS_SEPARATOR));
+ }
+
+ /**
+ * Parse a stored notification channels value into the corresponding enum set.
+ * Unknown tokens are skipped and a blank value yields an empty set.
+ *
+ * @param value Stored attribute value.
+ * @return Parsed channels.
+ */
+ private static Set parseNotificationChannels(String value) {
+
+ if (StringUtils.isBlank(value)) {
+ return Collections.emptySet();
+ }
+ Set channels =
+ EnumSet.noneOf(DeviceRegistrationNotificationChannelEnum.class);
+ for (String token : value.split(PushDeviceHandlerConstants.NOTIFICATION_CHANNELS_SEPARATOR)) {
+ String trimmed = token.trim();
+ if (trimmed.isEmpty()) {
+ continue;
+ }
+ try {
+ channels.add(DeviceRegistrationNotificationChannelEnum.valueOf(trimmed.toUpperCase(Locale.ENGLISH)));
+ } catch (IllegalArgumentException e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Unknown device registration notification channel: " + token, e);
+ }
+ }
+ }
+ return channels;
+ }
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstantsTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstantsTest.java
new file mode 100644
index 0000000..ef5758e
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstantsTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.constant;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG;
+
+/**
+ * Unit tests for PushDeviceHandlerConstants — covering newly added error codes.
+ */
+public class PushDeviceHandlerConstantsTest {
+
+ @Test
+ public void testErrorCodeGettingPushDeviceConfig_HasCorrectCode() {
+
+ Assert.assertEquals(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getCode(), "PDH-15014");
+ }
+
+ @Test
+ public void testErrorCodeGettingPushDeviceConfig_HasNonEmptyMessage() {
+
+ Assert.assertNotNull(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getMessage());
+ Assert.assertFalse(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getMessage().isEmpty());
+ }
+
+ @Test
+ public void testErrorCodeAddingPushDeviceConfig_HasCorrectCode() {
+
+ Assert.assertEquals(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getCode(), "PDH-15015");
+ }
+
+ @Test
+ public void testErrorCodeAddingPushDeviceConfig_HasNonEmptyMessage() {
+
+ Assert.assertNotNull(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage());
+ Assert.assertFalse(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage().isEmpty());
+ }
+
+ @Test
+ public void testErrorCodes_ToString_ContainsCodeAndMessage() {
+
+ String gettingConfigStr = ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.toString();
+ Assert.assertTrue(gettingConfigStr.contains("PDH-15014"),
+ "toString must include the error code.");
+ Assert.assertTrue(gettingConfigStr.contains(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getMessage()),
+ "toString must include the error message.");
+
+ String addingConfigStr = ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.toString();
+ Assert.assertTrue(addingConfigStr.contains("PDH-15015"),
+ "toString must include the error code.");
+ Assert.assertTrue(addingConfigStr.contains(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage()),
+ "toString must include the error message.");
+ }
+
+ @Test
+ public void testErrorCodes_AreDistinct() {
+
+ Assert.assertNotEquals(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getCode(),
+ ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getCode(),
+ "The two new error codes must have different code values.");
+ }
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImplTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImplTest.java
index 8376fbe..0e3fa29 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImplTest.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImplTest.java
@@ -219,6 +219,133 @@ public void testGetDeviceByUserId() throws Exception {
}
}
+ @Test
+ public void testGetDevicesByUserId() throws Exception {
+
+ String userId = "user";
+ int tenantId = 1;
+ Device device = new Device("id", "did", "name", "model", "token", "handle", "provider", "key");
+
+ try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) {
+ Connection connection = Mockito.mock(Connection.class);
+ NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class);
+ ResultSet resultSet = Mockito.mock(ResultSet.class);
+
+ mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection);
+ Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement);
+ Mockito.when(statement.executeQuery()).thenReturn(resultSet);
+ // Two devices then end of result set.
+ Mockito.when(resultSet.next()).thenReturn(true, true, false);
+ Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.ID))
+ .thenReturn(device.getDeviceId());
+ Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.USER_ID))
+ .thenReturn(device.getUserId());
+ Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_NAME))
+ .thenReturn(device.getDeviceName());
+ Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_MODEL))
+ .thenReturn(device.getDeviceModel());
+ Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_TOKEN))
+ .thenReturn(device.getDeviceToken());
+ Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_HANDLE))
+ .thenReturn(device.getDeviceHandle());
+ Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.PUBLIC_KEY))
+ .thenReturn(device.getPublicKey());
+ Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.PROVIDER))
+ .thenReturn(device.getProvider());
+
+ java.util.List result = deviceDAO.getDevicesByUserId(userId, tenantId);
+
+ Assert.assertEquals(result.size(), 2);
+ Assert.assertEquals(result.get(0).getDeviceId(), device.getDeviceId());
+ }
+ }
+
+ @Test
+ public void testGetDevicesByUserIdEmpty() throws Exception {
+
+ String userId = "user";
+ int tenantId = 1;
+
+ try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) {
+ Connection connection = Mockito.mock(Connection.class);
+ NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class);
+ ResultSet resultSet = Mockito.mock(ResultSet.class);
+
+ mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection);
+ Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement);
+ Mockito.when(statement.executeQuery()).thenReturn(resultSet);
+ Mockito.when(resultSet.next()).thenReturn(false);
+
+ java.util.List result = deviceDAO.getDevicesByUserId(userId, tenantId);
+
+ Assert.assertTrue(result.isEmpty());
+ }
+ }
+
+ @Test
+ public void testGetDeviceNotFound() throws Exception {
+
+ String deviceId = "id";
+
+ try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) {
+ Connection connection = Mockito.mock(Connection.class);
+ NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class);
+ ResultSet resultSet = Mockito.mock(ResultSet.class);
+
+ mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection);
+ Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement);
+ Mockito.when(statement.executeQuery()).thenReturn(resultSet);
+ Mockito.when(resultSet.next()).thenReturn(false);
+
+ Optional result = deviceDAO.getDevice(deviceId);
+
+ Assert.assertFalse(result.isPresent());
+ }
+ }
+
+ @Test
+ public void testGetDeviceByUserIdNotFound() throws Exception {
+
+ String userId = "user";
+ int tenantId = 1;
+
+ try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) {
+ Connection connection = Mockito.mock(Connection.class);
+ NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class);
+ ResultSet resultSet = Mockito.mock(ResultSet.class);
+
+ mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection);
+ Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement);
+ Mockito.when(statement.executeQuery()).thenReturn(resultSet);
+ Mockito.when(resultSet.next()).thenReturn(false);
+
+ Optional result = deviceDAO.getDeviceByUserId(userId, tenantId);
+
+ Assert.assertFalse(result.isPresent());
+ }
+ }
+
+ @Test
+ public void testGetPublicKeyNotFound() throws Exception {
+
+ String deviceId = "id";
+
+ try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) {
+ Connection connection = Mockito.mock(Connection.class);
+ NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class);
+ ResultSet resultSet = Mockito.mock(ResultSet.class);
+
+ mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection);
+ Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement);
+ Mockito.when(statement.executeQuery()).thenReturn(resultSet);
+ Mockito.when(resultSet.next()).thenReturn(false);
+
+ Optional result = deviceDAO.getPublicKey(deviceId);
+
+ Assert.assertFalse(result.isPresent());
+ }
+ }
+
@Test
public void testGetPublicKey() throws Exception {
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImplTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImplTest.java
index ed63812..99bf7f0 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImplTest.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImplTest.java
@@ -29,12 +29,19 @@
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeTest;
+import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.wso2.carbon.context.CarbonContext;
import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils;
+import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager;
+import org.wso2.carbon.identity.core.context.IdentityContext;
+import org.wso2.carbon.identity.core.context.model.Header;
+import org.wso2.carbon.identity.core.context.model.Request;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
+import org.wso2.carbon.identity.event.IdentityEventException;
+import org.wso2.carbon.identity.event.services.IdentityEventService;
+import org.wso2.carbon.identity.governance.service.notification.NotificationChannels;
import org.wso2.carbon.identity.notification.push.device.handler.DeviceRegistrationContextManager;
import org.wso2.carbon.identity.notification.push.device.handler.dao.DeviceDAO;
import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerClientException;
@@ -60,6 +67,7 @@
import org.wso2.carbon.user.core.UserRealm;
import org.wso2.carbon.user.core.UserStoreException;
import org.wso2.carbon.user.core.common.AbstractUserStoreManager;
+import org.wso2.carbon.user.core.common.User;
import org.wso2.carbon.user.core.util.UserCoreUtil;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;
@@ -189,7 +197,7 @@ public static void tearDownClass() {
}
}
- @BeforeTest
+ @BeforeMethod
void setUp() {
MockitoAnnotations.openMocks(this);
@@ -248,6 +256,8 @@ public void testRegisterDevice()
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -340,6 +350,8 @@ public void testRegisterDeviceWithUserIdBlank() throws UserStoreException {
try (
MockedStatic mockedIdentityTenantUtil =
Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
) {
mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
@@ -350,6 +362,12 @@ public void testRegisterDeviceWithUserIdBlank() throws UserStoreException {
when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn(null);
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+
Assert.assertThrows(PushDeviceHandlerServerException.class, () -> {
deviceHandlerService.registerDevice(registrationRequest, "carbon.super");
});
@@ -369,6 +387,8 @@ public void testRegisterDeviceWithExistingDevice() throws UserStoreException, Pu
try (
MockedStatic mockedIdentityTenantUtil =
Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
) {
mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
@@ -379,8 +399,14 @@ public void testRegisterDeviceWithExistingDevice() throws UserStoreException, Pu
when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
- Optional device = Optional.of(new Device());
- when(deviceDAO.getDeviceByUserId(anyString(), anyInt())).thenReturn(device);
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt()))
+ .thenReturn(java.util.Collections.singletonList(new Device()));
Assert.assertThrows(PushDeviceHandlerClientException.class, () -> {
deviceHandlerService.registerDevice(registrationRequest, "carbon.super");
@@ -466,6 +492,44 @@ public void testGetDeviceByUserIdFailure() throws PushDeviceHandlerException {
}
}
+ @Test
+ public void testGetDevicesByUserId() throws PushDeviceHandlerException {
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+
+ Device deviceObj = new Device();
+ deviceObj.setDeviceId("1234567890");
+ List deviceList = new ArrayList<>();
+ deviceList.add(deviceObj);
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(deviceList);
+
+ List result = deviceHandlerService.getDevicesByUserId("testUserId", "carbon.super");
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ }
+ }
+
+ @Test
+ public void testGetDevicesByUserIdWhenNoDevices() throws PushDeviceHandlerException {
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>());
+
+ // No registered devices is a valid result and must return an empty list, not throw.
+ List result = deviceHandlerService.getDevicesByUserId("testUserId", "carbon.super");
+ Assert.assertNotNull(result);
+ Assert.assertTrue(result.isEmpty());
+ }
+ }
+
@Test
public void testUnregisterDevice()
throws PushDeviceHandlerException, NotificationSenderManagementException, PushProviderException {
@@ -1097,6 +1161,8 @@ public void testRegisterDevicePushProviderClientException()
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1131,6 +1197,170 @@ public void testRegisterDevicePushProviderClientException()
}
}
+ @Test
+ public void testRegisterDeviceWithAlreadyRegisteredDeviceId()
+ throws Exception {
+
+ RegistrationRequest registrationRequest = createRegistrationRequest();
+ DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext();
+ when(deviceRegistrationContextManager.getContext(anyString(), anyString()))
+ .thenReturn(deviceRegistrationContext);
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+ UserRealm userRealm = mock(UserRealm.class);
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString()))
+ .thenReturn(userRealm);
+ AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
+
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+
+ // Multiple device enrollment disabled, so a user that already has a registered device
+ // must not be allowed to register another one.
+ org.wso2.carbon.identity.configuration.mgt.core.model.Resource resource =
+ new org.wso2.carbon.identity.configuration.mgt.core.model.Resource();
+ List attributes = new ArrayList<>();
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "enableMultipleDeviceEnrollment", "false"));
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "maximumDeviceLimit", "1"));
+ resource.setAttributes(attributes);
+ when(configManager.getResource(anyString(), anyString(), anyBoolean())).thenReturn(resource);
+
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt()))
+ .thenReturn(java.util.Collections.singletonList(new Device()));
+
+ Assert.assertThrows(PushDeviceHandlerClientException.class,
+ () -> deviceHandlerService.registerDevice(registrationRequest, "carbon.super"));
+ }
+ }
+
+ @Test
+ public void testRegisterDeviceWhenMultiDeviceEnabledAndLimitReached()
+ throws Exception {
+
+ RegistrationRequest registrationRequest = createRegistrationRequest();
+ DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext();
+ when(deviceRegistrationContextManager.getContext(anyString(), anyString()))
+ .thenReturn(deviceRegistrationContext);
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+ UserRealm userRealm = mock(UserRealm.class);
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString()))
+ .thenReturn(userRealm);
+ AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
+
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+
+ org.wso2.carbon.identity.configuration.mgt.core.model.Resource resource =
+ new org.wso2.carbon.identity.configuration.mgt.core.model.Resource();
+ List attributes = new ArrayList<>();
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "enableMultipleDeviceEnrollment", "true"));
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "maximumDeviceLimit", "2"));
+ resource.setAttributes(attributes);
+ when(configManager.getResource(anyString(), anyString(), anyBoolean())).thenReturn(resource);
+
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt()))
+ .thenReturn(java.util.Arrays.asList(new Device(), new Device()));
+
+ Assert.assertThrows(PushDeviceHandlerClientException.class,
+ () -> deviceHandlerService.registerDevice(registrationRequest, "carbon.super"));
+ }
+ }
+
+ @Test
+ public void testRegisterDeviceWhenMultiDeviceEnabledAndUnderLimit()
+ throws Exception {
+
+ RegistrationRequest registrationRequest = createRegistrationRequest();
+ DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext();
+ when(deviceRegistrationContextManager.getContext(anyString(), anyString()))
+ .thenReturn(deviceRegistrationContext);
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+ UserRealm userRealm = mock(UserRealm.class);
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString()))
+ .thenReturn(userRealm);
+ AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
+
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+
+ org.wso2.carbon.identity.configuration.mgt.core.model.Resource resource =
+ new org.wso2.carbon.identity.configuration.mgt.core.model.Resource();
+ List attributes = new ArrayList<>();
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "enableMultipleDeviceEnrollment", "true"));
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "maximumDeviceLimit", "5"));
+ resource.setAttributes(attributes);
+ when(configManager.getResource(anyString(), anyString(), anyBoolean())).thenReturn(resource);
+
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt()))
+ .thenReturn(java.util.Collections.singletonList(new Device()));
+
+ NotificationSenderManagementService notificationSenderManagementService =
+ mock(NotificationSenderManagementService.class);
+ when(pushDeviceHandlerDataHolder.getNotificationSenderManagementService())
+ .thenReturn(notificationSenderManagementService);
+ PushSenderDTO pushSenderDTO = new PushSenderDTO();
+ pushSenderDTO.setName("FCM_PushPublisher");
+ pushSenderDTO.setProvider("FCM");
+ pushSenderDTO.setProviderId("fcm-provider-id");
+ List pushSenders = new ArrayList<>();
+ pushSenders.add(pushSenderDTO);
+ when(notificationSenderManagementService.getPushSenders(anyBoolean()))
+ .thenReturn(pushSenders);
+
+ FCMPushProvider fcmPushProvider = mock(FCMPushProvider.class);
+ when(pushDeviceHandlerDataHolder.getPushProvider(anyString())).thenReturn(fcmPushProvider);
+ when(fcmPushProvider.getName()).thenReturn("FCM");
+ doNothing().when(fcmPushProvider).registerDevice(any(), any());
+ doNothing().when(deviceDAO).registerDevice(any(), anyInt());
+
+ Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super");
+
+ Assert.assertNotNull(registeredDevice);
+ Assert.assertTrue(deviceRegistrationContext.isRegistered());
+ }
+ }
+
private DeviceRegistrationContext createDeviceRegistrationContext() {
return new DeviceRegistrationContext(
@@ -1179,10 +1409,13 @@ public void testRegisterDeviceWithExplicitProviderData()
when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
when(deviceDAO.getDeviceByUserId(anyString(), anyInt())).thenReturn(Optional.empty());
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>());
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1244,6 +1477,8 @@ public void testRegisterDeviceWhenNotificationSenderServiceFails() throws Except
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(PushDeviceHandlerDataHolder::getInstance)
.thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1303,6 +1538,8 @@ public void testRegisterDeviceWhenNoPushSenderFoundForProvider() throws Exceptio
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(PushDeviceHandlerDataHolder::getInstance)
.thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1360,6 +1597,8 @@ public void testRegisterDeviceWhenPushSenderDoesNotMatchProvider() throws Except
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(PushDeviceHandlerDataHolder::getInstance)
.thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1424,10 +1663,13 @@ public void testRegisterDeviceWithoutProviderData()
when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
when(deviceDAO.getDeviceByUserId(anyString(), anyInt())).thenReturn(Optional.empty());
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>());
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1503,6 +1745,8 @@ public void testRegisterDeviceFailsWithNoProviderAndNoDefault() throws UserStore
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1559,6 +1803,8 @@ public void testRegisterDeviceFailsWhenDefaultProviderRetrievalFails() throws Us
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1611,10 +1857,13 @@ public void testRegisterDeviceWithBlankProviderNameFallsBackToDefault()
when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
when(deviceDAO.getDeviceByUserId(anyString(), anyInt())).thenReturn(Optional.empty());
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>());
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1688,6 +1937,8 @@ public void testRegisterDeviceWithConfigMissingDefaultKeyFallsBackToSenderCheck(
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1747,6 +1998,8 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultSetsOnlyAvailableSenderA
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1756,6 +2009,7 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultSetsOnlyAvailableSenderA
// Return empty config to simulate no default provider configured
when(notificationSenderManagementService.getNotificationSenderConfigurations(anyString(), anyBoolean()))
.thenReturn(new HashMap<>());
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>());
// Mock single push sender available
PushSenderDTO pushSenderDTO = new PushSenderDTO();
@@ -1819,6 +2073,8 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenNoPushSendersAv
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1828,6 +2084,7 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenNoPushSendersAv
// Return empty config to simulate no default provider configured
when(notificationSenderManagementService.getNotificationSenderConfigurations(anyString(), anyBoolean()))
.thenReturn(new HashMap<>());
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>());
// Return empty list of push senders
when(notificationSenderManagementService.getPushSenders(anyBoolean()))
@@ -1875,6 +2132,8 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenMultiplePushSen
PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
mockedPushDeviceHandlerDataHolder.when(
PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
NotificationSenderManagementService notificationSenderManagementService =
mock(NotificationSenderManagementService.class);
@@ -1884,6 +2143,7 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenMultiplePushSen
// Return empty config to simulate no default provider configured
when(notificationSenderManagementService.getNotificationSenderConfigurations(anyString(), anyBoolean()))
.thenReturn(new HashMap<>());
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>());
// Return multiple push senders
PushSenderDTO pushSenderDTO1 = new PushSenderDTO();
@@ -1898,12 +2158,330 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenMultiplePushSen
when(notificationSenderManagementService.getPushSenders(anyBoolean()))
.thenReturn(pushSenders);
- Assert.assertThrows(PushDeviceHandlerServerException.class, () -> {
- deviceHandlerService.registerDevice(registrationRequest, "carbon.super");
- });
+ Assert.assertThrows(PushDeviceHandlerServerException.class,
+ () -> deviceHandlerService.registerDevice(registrationRequest, "carbon.super"));
+ }
+ }
+
+ @Test
+ public void testRegisterDeviceTriggersRegistrationNotificationsWhenEnabled() throws Exception {
+
+ RegistrationRequest registrationRequest = createRegistrationRequest();
+ DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext();
+ when(deviceRegistrationContextManager.getContext(anyString(), anyString()))
+ .thenReturn(deviceRegistrationContext);
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
+ MockedStatic mockedIdentityContext =
+ Mockito.mockStatic(IdentityContext.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+ UserRealm userRealm = mock(UserRealm.class);
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString()))
+ .thenReturn(userRealm);
+ AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
+
+ User user = mock(User.class);
+ when(user.getUserID()).thenReturn("testUserId");
+ when(user.getUsername()).thenReturn("testUser");
+ when(user.getUserStoreDomain()).thenReturn("PRIMARY");
+ Map userAttributes = new HashMap<>();
+ userAttributes.put(NotificationChannels.EMAIL_CHANNEL.getClaimUri(), "testuser@wso2.com");
+ when(user.getAttributes()).thenReturn(userAttributes);
+ when(abstractUserStoreManager.getUserWithID(anyString(), any(String[].class), anyString()))
+ .thenReturn(user);
+
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+ when(configManager.getResource(anyString(), anyString(), anyBoolean()))
+ .thenReturn(createDeviceMgtConfigResource("true", "5", "true", "EMAIL,PUSH_NOTIFICATION"));
+
+ Device existingDevice = new Device();
+ existingDevice.setDeviceId("existing-device-id");
+ existingDevice.setProvider("FCM");
+ existingDevice.setDeviceToken("existing-device-token");
+ existingDevice.setDeviceHandle("existing-device-handle");
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt()))
+ .thenReturn(java.util.Collections.singletonList(existingDevice));
+
+ mockPushSenderAndProvider(pushDeviceHandlerDataHolder);
+ doNothing().when(deviceDAO).registerDevice(any(), anyInt());
+
+ IdentityEventService identityEventService = mock(IdentityEventService.class);
+ when(pushDeviceHandlerDataHolder.getIdentityEventService()).thenReturn(identityEventService);
+ doNothing().when(identityEventService).handleEvent(any());
+
+ IdentityContext identityContext = mock(IdentityContext.class);
+ mockedIdentityContext.when(IdentityContext::getThreadLocalIdentityContext)
+ .thenReturn(identityContext);
+ Request request = mock(Request.class);
+ List headers = new ArrayList<>();
+ headers.add(new Header("User-Agent", java.util.Collections.singletonList("test-agent")));
+ headers.add(new Header("X-Forwarded-For",
+ java.util.Collections.singletonList("10.0.0.1, 192.168.1.1")));
+ when(request.getHeaders()).thenReturn(headers);
+ when(identityContext.getRequest()).thenReturn(request);
+
+ Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super");
+
+ Assert.assertNotNull(registeredDevice);
+ Assert.assertTrue(deviceRegistrationContext.isRegistered());
+ // One email notification plus one push notification to the existing device.
+ verify(identityEventService, times(2)).handleEvent(any());
+ }
+ }
+
+ @Test
+ public void testRegisterDeviceSkipsNotificationsWhenEmailClaimNotAvailable() throws Exception {
+
+ RegistrationRequest registrationRequest = createRegistrationRequest();
+ DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext();
+ when(deviceRegistrationContextManager.getContext(anyString(), anyString()))
+ .thenReturn(deviceRegistrationContext);
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+ UserRealm userRealm = mock(UserRealm.class);
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString()))
+ .thenReturn(userRealm);
+ AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
+ // The resolved user has no email claim, so the email notification must be
+ // skipped and registration must still succeed.
+ User user = mock(User.class);
+ when(user.getUserID()).thenReturn("testUserId");
+ when(user.getUsername()).thenReturn("testUser");
+ when(user.getUserStoreDomain()).thenReturn("PRIMARY");
+ when(user.getAttributes()).thenReturn(new HashMap<>());
+ when(abstractUserStoreManager.getUserWithID(anyString(), any(String[].class), anyString()))
+ .thenReturn(user);
+
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+ when(configManager.getResource(anyString(), anyString(), anyBoolean()))
+ .thenReturn(createDeviceMgtConfigResource("true", "5", "true", "EMAIL,PUSH_NOTIFICATION"));
+
+ // No previously registered devices, so the push notification is skipped too.
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>());
+
+ mockPushSenderAndProvider(pushDeviceHandlerDataHolder);
+ doNothing().when(deviceDAO).registerDevice(any(), anyInt());
+
+ IdentityEventService identityEventService = mock(IdentityEventService.class);
+ when(pushDeviceHandlerDataHolder.getIdentityEventService()).thenReturn(identityEventService);
+
+ Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super");
+
+ Assert.assertNotNull(registeredDevice);
+ Assert.assertTrue(deviceRegistrationContext.isRegistered());
+ verify(identityEventService, times(0)).handleEvent(any());
+ }
+ }
+
+ @Test
+ public void testRegisterDeviceSucceedsWhenNotificationDeliveryFails() throws Exception {
+
+ RegistrationRequest registrationRequest = createRegistrationRequest();
+ DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext();
+ when(deviceRegistrationContextManager.getContext(anyString(), anyString()))
+ .thenReturn(deviceRegistrationContext);
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
+ MockedStatic mockedIdentityContext =
+ Mockito.mockStatic(IdentityContext.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+ UserRealm userRealm = mock(UserRealm.class);
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString()))
+ .thenReturn(userRealm);
+ AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
+
+ User user = mock(User.class);
+ when(user.getUserID()).thenReturn("testUserId");
+ when(user.getUsername()).thenReturn("testUser");
+ when(user.getUserStoreDomain()).thenReturn("PRIMARY");
+ Map userAttributes = new HashMap<>();
+ userAttributes.put(NotificationChannels.EMAIL_CHANNEL.getClaimUri(), "testuser@wso2.com");
+ when(user.getAttributes()).thenReturn(userAttributes);
+ when(abstractUserStoreManager.getUserWithID(anyString(), any(String[].class), anyString()))
+ .thenReturn(user);
+
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+ when(configManager.getResource(anyString(), anyString(), anyBoolean()))
+ .thenReturn(createDeviceMgtConfigResource("true", "5", "true", "EMAIL,PUSH_NOTIFICATION"));
+
+ Device existingDevice = new Device();
+ existingDevice.setDeviceId("existing-device-id");
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt()))
+ .thenReturn(java.util.Collections.singletonList(existingDevice));
+
+ mockPushSenderAndProvider(pushDeviceHandlerDataHolder);
+ doNothing().when(deviceDAO).registerDevice(any(), anyInt());
+
+ IdentityEventService identityEventService = mock(IdentityEventService.class);
+ when(pushDeviceHandlerDataHolder.getIdentityEventService()).thenReturn(identityEventService);
+ doThrow(new IdentityEventException("Notification delivery failed."))
+ .when(identityEventService).handleEvent(any());
+
+ // No inbound request captured in the identity context; the IP address
+ // cannot be resolved and the notification is sent without it.
+ IdentityContext identityContext = mock(IdentityContext.class);
+ mockedIdentityContext.when(IdentityContext::getThreadLocalIdentityContext)
+ .thenReturn(identityContext);
+ when(identityContext.getRequest()).thenReturn(null);
+
+ Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super");
+
+ Assert.assertNotNull(registeredDevice);
+ Assert.assertTrue(deviceRegistrationContext.isRegistered());
+ verify(identityEventService, times(2)).handleEvent(any());
+ }
+ }
+
+ @Test
+ public void testRegisterDeviceEmitsDiagnosticLogsWhenNotificationDeliveryFails() throws Exception {
+
+ RegistrationRequest registrationRequest = createRegistrationRequest();
+ DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext();
+ when(deviceRegistrationContextManager.getContext(anyString(), anyString()))
+ .thenReturn(deviceRegistrationContext);
+
+ try (
+ MockedStatic mockedIdentityTenantUtil =
+ Mockito.mockStatic(IdentityTenantUtil.class);
+ MockedStatic mockedPushDeviceHandlerDataHolder =
+ Mockito.mockStatic(PushDeviceHandlerDataHolder.class);
+ MockedStatic mockedIdentityContext =
+ Mockito.mockStatic(IdentityContext.class);
+ ) {
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234);
+ UserRealm userRealm = mock(UserRealm.class);
+ mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString()))
+ .thenReturn(userRealm);
+ AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId");
+
+ // Enable diagnostic logs so the notification failure paths emit diagnostic log events.
+ mockedLoggerUtils.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(true);
+
+ User user = mock(User.class);
+ when(user.getUserID()).thenReturn("testUserId");
+ when(user.getUsername()).thenReturn("testUser");
+ when(user.getUserStoreDomain()).thenReturn("PRIMARY");
+ Map userAttributes = new HashMap<>();
+ userAttributes.put(NotificationChannels.EMAIL_CHANNEL.getClaimUri(), "testuser@wso2.com");
+ when(user.getAttributes()).thenReturn(userAttributes);
+ when(abstractUserStoreManager.getUserWithID(anyString(), any(String[].class), anyString()))
+ .thenReturn(user);
+
+ PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class);
+ mockedPushDeviceHandlerDataHolder.when(
+ PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder);
+ ConfigurationManager configManager = mock(ConfigurationManager.class);
+ when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager);
+ when(configManager.getResource(anyString(), anyString(), anyBoolean()))
+ .thenReturn(createDeviceMgtConfigResource("true", "5", "true", "EMAIL,PUSH_NOTIFICATION"));
+
+ Device existingDevice = new Device();
+ existingDevice.setDeviceId("existing-device-id");
+ when(deviceDAO.getDevicesByUserId(anyString(), anyInt()))
+ .thenReturn(java.util.Collections.singletonList(existingDevice));
+
+ mockPushSenderAndProvider(pushDeviceHandlerDataHolder);
+ doNothing().when(deviceDAO).registerDevice(any(), anyInt());
+
+ IdentityEventService identityEventService = mock(IdentityEventService.class);
+ when(pushDeviceHandlerDataHolder.getIdentityEventService()).thenReturn(identityEventService);
+ doThrow(new IdentityEventException("Notification delivery failed."))
+ .when(identityEventService).handleEvent(any());
+
+ // An inbound request is captured in the identity context so the IP address
+ // input parameter is included in the emitted diagnostic logs.
+ IdentityContext identityContext = mock(IdentityContext.class);
+ mockedIdentityContext.when(IdentityContext::getThreadLocalIdentityContext)
+ .thenReturn(identityContext);
+ org.wso2.carbon.identity.core.context.model.Request request =
+ mock(org.wso2.carbon.identity.core.context.model.Request.class);
+ when(request.getIpAddress()).thenReturn("10.0.0.1");
+ when(identityContext.getRequest()).thenReturn(request);
+
+ Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super");
+
+ Assert.assertNotNull(registeredDevice);
+ Assert.assertTrue(deviceRegistrationContext.isRegistered());
+ verify(identityEventService, times(2)).handleEvent(any());
}
}
+ private void mockPushSenderAndProvider(PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder)
+ throws NotificationSenderManagementException, PushProviderException {
+
+ NotificationSenderManagementService notificationSenderManagementService =
+ mock(NotificationSenderManagementService.class);
+ when(pushDeviceHandlerDataHolder.getNotificationSenderManagementService())
+ .thenReturn(notificationSenderManagementService);
+ PushSenderDTO pushSenderDTO = new PushSenderDTO();
+ pushSenderDTO.setName("FCM_PushPublisher");
+ pushSenderDTO.setProvider("FCM");
+ pushSenderDTO.setProviderId("fcm-provider-id");
+ List pushSenders = new ArrayList<>();
+ pushSenders.add(pushSenderDTO);
+ when(notificationSenderManagementService.getPushSenders(anyBoolean())).thenReturn(pushSenders);
+
+ FCMPushProvider fcmPushProvider = mock(FCMPushProvider.class);
+ when(pushDeviceHandlerDataHolder.getPushProvider(anyString())).thenReturn(fcmPushProvider);
+ when(fcmPushProvider.getName()).thenReturn("FCM");
+ doNothing().when(fcmPushProvider).registerDevice(any(), any());
+ }
+
+ private org.wso2.carbon.identity.configuration.mgt.core.model.Resource createDeviceMgtConfigResource(
+ String enableMultipleDeviceEnrollment, String maximumDeviceLimit,
+ String enableNotifications, String notificationChannels) {
+
+ org.wso2.carbon.identity.configuration.mgt.core.model.Resource resource =
+ new org.wso2.carbon.identity.configuration.mgt.core.model.Resource();
+ List attributes = new ArrayList<>();
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "enableMultipleDeviceEnrollment", enableMultipleDeviceEnrollment));
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "maximumDeviceLimit", maximumDeviceLimit));
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "enableDeviceRegistrationNotifications", enableNotifications));
+ attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute(
+ "deviceRegistrationNotificationChannels", notificationChannels));
+ resource.setAttributes(attributes);
+ return resource;
+ }
+
private RegistrationRequest createRegistrationRequest() {
RegistrationRequest registrationRequest = new RegistrationRequest();
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImplTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImplTest.java
new file mode 100644
index 0000000..73eb6f3
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImplTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.impl;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.wso2.carbon.identity.configuration.mgt.core.model.Attribute;
+import org.wso2.carbon.identity.configuration.mgt.core.model.Resource;
+import org.wso2.carbon.identity.configuration.mgt.core.model.ResourceIdentifier;
+import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants;
+import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationNotificationChannelEnum;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Unit tests for PushDefaultConfigResolverImpl.
+ */
+public class PushDefaultConfigResolverImplTest {
+
+ private PushDefaultConfigResolverImpl pushDefaultConfigResolver;
+
+ @BeforeMethod
+ public void setUp() {
+
+ pushDefaultConfigResolver = new PushDefaultConfigResolverImpl();
+ }
+
+ @Test
+ public void testGetResourceIdentifier_ReturnsExpectedTypeAndName() {
+
+ ResourceIdentifier identifier = pushDefaultConfigResolver.getResourceIdentifier();
+
+ Assert.assertNotNull(identifier, "ResourceIdentifier should not be null.");
+ Assert.assertEquals(identifier.getResourceType(), PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE);
+ Assert.assertEquals(identifier.getResourceName(), PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testGetDefaultConfigs_ReturnsDefaultPushDeviceMgtConfigs() {
+
+ Resource resource = pushDefaultConfigResolver.getDefaultConfigs(
+ PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE,
+ PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME);
+
+ Assert.assertNotNull(resource, "Default config resource should not be null.");
+ Assert.assertEquals(resource.getResourceName(), PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME);
+ Assert.assertEquals(resource.getResourceType(), PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE);
+
+ Map attributes = resource.getAttributes().stream()
+ .collect(Collectors.toMap(Attribute::getKey, Attribute::getValue));
+ Assert.assertEquals(attributes.get(PushDeviceHandlerConstants.ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT), "false");
+ Assert.assertEquals(attributes.get(PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT), "2");
+ Assert.assertEquals(
+ attributes.get(PushDeviceHandlerConstants.ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS),
+ "false");
+ Assert.assertEquals(
+ attributes.get(PushDeviceHandlerConstants.ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS),
+ DeviceRegistrationNotificationChannelEnum.EMAIL.name());
+ }
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolderTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolderTest.java
new file mode 100644
index 0000000..1f7d6fa
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolderTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.internal;
+
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager;
+
+import static org.mockito.Mockito.mock;
+
+/**
+ * Unit tests for PushDeviceHandlerDataHolder.
+ */
+public class PushDeviceHandlerDataHolderTest {
+
+ private PushDeviceHandlerDataHolder dataHolder;
+
+ @BeforeMethod
+ public void setUp() {
+
+ dataHolder = PushDeviceHandlerDataHolder.getInstance();
+ dataHolder.setConfigurationManager(null);
+ }
+
+ @AfterMethod
+ public void tearDown() {
+
+ dataHolder.setConfigurationManager(null);
+ }
+
+ @Test
+ public void testGetInstance_ReturnsSingleton() {
+
+ PushDeviceHandlerDataHolder instance1 = PushDeviceHandlerDataHolder.getInstance();
+ PushDeviceHandlerDataHolder instance2 = PushDeviceHandlerDataHolder.getInstance();
+ Assert.assertNotNull(instance1);
+ Assert.assertSame(instance1, instance2, "getInstance must always return the same singleton.");
+ }
+
+ @Test
+ public void testSetAndGetConfigurationManager_StoresAndRetrievesCorrectly() {
+
+ ConfigurationManager mockManager = mock(ConfigurationManager.class);
+
+ dataHolder.setConfigurationManager(mockManager);
+ ConfigurationManager result = dataHolder.getConfigurationManager();
+
+ Assert.assertSame(result, mockManager, "getConfigurationManager must return the same instance that was set.");
+ }
+
+ @Test
+ public void testGetConfigurationManager_ReturnsNull_WhenNotSet() {
+
+ ConfigurationManager result = dataHolder.getConfigurationManager();
+
+ Assert.assertNull(result, "getConfigurationManager must return null when no manager has been set.");
+ }
+
+ @Test
+ public void testSetConfigurationManager_AllowsNull() {
+
+ ConfigurationManager mockManager = mock(ConfigurationManager.class);
+ dataHolder.setConfigurationManager(mockManager);
+
+ dataHolder.setConfigurationManager(null);
+
+ Assert.assertNull(dataHolder.getConfigurationManager(),
+ "setConfigurationManager(null) must clear the stored manager.");
+ }
+
+ @Test
+ public void testSetConfigurationManager_AllowsReplacement() {
+
+ ConfigurationManager firstManager = mock(ConfigurationManager.class);
+ ConfigurationManager secondManager = mock(ConfigurationManager.class);
+
+ dataHolder.setConfigurationManager(firstManager);
+ dataHolder.setConfigurationManager(secondManager);
+
+ Assert.assertSame(dataHolder.getConfigurationManager(), secondManager,
+ "Second manager must replace the first one.");
+ }
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponentTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponentTest.java
new file mode 100644
index 0000000..0d98889
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponentTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.internal;
+
+import org.mockito.MockedStatic;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Unit tests for PushDeviceHandlerServiceComponent — covering the ConfigurationManager OSGi lifecycle methods.
+ */
+public class PushDeviceHandlerServiceComponentTest {
+
+ private PushDeviceHandlerServiceComponent serviceComponent;
+ private PushDeviceHandlerDataHolder mockDataHolderInstance;
+ private MockedStatic mockedDataHolder;
+
+ @BeforeMethod
+ public void setUp() {
+
+ serviceComponent = new PushDeviceHandlerServiceComponent();
+ mockDataHolderInstance = mock(PushDeviceHandlerDataHolder.class);
+ mockedDataHolder = mockStatic(PushDeviceHandlerDataHolder.class);
+ mockedDataHolder.when(PushDeviceHandlerDataHolder::getInstance).thenReturn(mockDataHolderInstance);
+ }
+
+ @AfterMethod
+ public void tearDown() {
+
+ mockedDataHolder.close();
+ }
+
+ @Test
+ public void testSetConfigurationManager_DelegatesToDataHolder() {
+
+ ConfigurationManager mockManager = mock(ConfigurationManager.class);
+
+ serviceComponent.setConfigurationManager(mockManager);
+
+ verify(mockDataHolderInstance).setConfigurationManager(mockManager);
+ }
+
+ @Test
+ public void testUnsetConfigurationManager_ClearsDataHolderEntry() {
+
+ ConfigurationManager mockManager = mock(ConfigurationManager.class);
+
+ serviceComponent.unsetConfigurationManager(mockManager);
+
+ verify(mockDataHolderInstance).setConfigurationManager(null);
+ }
+
+ @Test
+ public void testSetThenUnsetConfigurationManager_LeavesDataHolderNull() {
+
+ ConfigurationManager mockManager = mock(ConfigurationManager.class);
+
+ serviceComponent.setConfigurationManager(mockManager);
+ serviceComponent.unsetConfigurationManager(mockManager);
+
+ verify(mockDataHolderInstance).setConfigurationManager(mockManager);
+ verify(mockDataHolderInstance).setConfigurationManager(null);
+ }
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLoggerTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLoggerTest.java
index 863fd22..5cac41a 100644
--- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLoggerTest.java
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLoggerTest.java
@@ -124,21 +124,63 @@ public void testGetUserWithSystemUser() throws Exception {
}
/**
- * Test the private method 'createAuditLogEntry' with valid data.
+ * Test the private method 'createAuditLogEntry' for UNREGISTER_DEVICE operation.
*/
@Test
- public void testCreateAuditLogEntryWithValidData() throws Exception {
+ public void testCreateAuditLogEntryForUnregisterDevice() throws Exception {
Method createAuditLogEntryMethod = DeviceHandlerAuditLogger.class
- .getDeclaredMethod("createAuditLogEntry", String.class);
+ .getDeclaredMethod("createAuditLogEntry", String.class, DeviceHandlerAuditLogger.Operation.class,
+ String.class);
createAuditLogEntryMethod.setAccessible(true);
- JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke(auditLogger, "testUserId");
+ JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke(
+ auditLogger, "testUserId", DeviceHandlerAuditLogger.Operation.UNREGISTER_DEVICE, "testDeviceId");
Assert.assertNotNull(result);
Assert.assertTrue(result.has("UserId"));
Assert.assertEquals(result.getString("UserId"), "testUserId");
Assert.assertTrue(result.has("UnregisteredAt"));
Assert.assertTrue(result.getLong("UnregisteredAt") > 0);
+ Assert.assertFalse(result.has("RegisteredAt"));
+ }
+
+ /**
+ * Test the private method 'createAuditLogEntry' for REGISTER_DEVICE operation.
+ */
+ @Test
+ public void testCreateAuditLogEntryForRegisterDevice() throws Exception {
+
+ Method createAuditLogEntryMethod = DeviceHandlerAuditLogger.class
+ .getDeclaredMethod("createAuditLogEntry", String.class, DeviceHandlerAuditLogger.Operation.class,
+ String.class);
+ createAuditLogEntryMethod.setAccessible(true);
+ JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke(
+ auditLogger, "testUserId", DeviceHandlerAuditLogger.Operation.REGISTER_DEVICE, "testDeviceId");
+
+ Assert.assertNotNull(result);
+ Assert.assertTrue(result.has("UserId"));
+ Assert.assertEquals(result.getString("UserId"), "testUserId");
+ Assert.assertTrue(result.has("RegisteredAt"));
+ Assert.assertTrue(result.getLong("RegisteredAt") > 0);
+ Assert.assertFalse(result.has("UnregisteredAt"));
+ }
+
+ /**
+ * Test the private method 'createAuditLogEntry' with a null userId.
+ */
+ @Test
+ public void testCreateAuditLogEntryWithNullUserId() throws Exception {
+
+ Method createAuditLogEntryMethod = DeviceHandlerAuditLogger.class
+ .getDeclaredMethod("createAuditLogEntry", String.class, DeviceHandlerAuditLogger.Operation.class,
+ String.class);
+ createAuditLogEntryMethod.setAccessible(true);
+ JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke(
+ auditLogger, (Object) null, DeviceHandlerAuditLogger.Operation.UNREGISTER_DEVICE, "testDeviceId");
+
+ Assert.assertNotNull(result);
+ Assert.assertTrue(result.has("UserId"));
+ Assert.assertTrue(result.isNull("UserId"));
}
/**
@@ -188,4 +230,50 @@ public void testGetInitiatorIdSystemUser() throws Exception {
String result = (String) getInitiatorIdMethod.invoke(auditLogger);
Assert.assertEquals(result, LoggerUtils.Initiator.System.name());
}
+
+ /**
+ * Test the private method 'getInitiatorId' falls back to masked content when the resolved initiator is blank
+ * and the user is not the system user.
+ */
+ @Test
+ public void testGetInitiatorIdMaskedContent() throws Exception {
+
+ when(carbonContext.getUsername()).thenReturn("testUser");
+ when(carbonContext.getTenantDomain()).thenReturn("carbon.super");
+ mockedUserCoreUtil.when(() -> UserCoreUtil.addTenantDomainToEntry("testUser", "carbon.super"))
+ .thenReturn("testUser@carbon.super");
+ mockedMultitenantUtils.when(() -> MultitenantUtils.getTenantAwareUsername("testUser@carbon.super"))
+ .thenReturn("testUser");
+ mockedMultitenantUtils.when(() -> MultitenantUtils.getTenantDomain("testUser@carbon.super"))
+ .thenReturn("carbon.super");
+ mockedIdentityUtil.when(() -> IdentityUtil.getInitiatorId("testUser", "carbon.super"))
+ .thenReturn(null);
+
+ Method getInitiatorIdMethod = DeviceHandlerAuditLogger.class.getDeclaredMethod("getInitiatorId");
+ getInitiatorIdMethod.setAccessible(true);
+ String result = (String) getInitiatorIdMethod.invoke(auditLogger);
+ Assert.assertEquals(result, "masked-content");
+ }
+
+ /**
+ * Test the private method 'createAuditLogEntry' for UPDATE_DEVICE_MGT_CONFIG operation and a null deviceId.
+ */
+ @Test
+ public void testCreateAuditLogEntryForUpdateConfigWithNullDeviceId() throws Exception {
+
+ Method createAuditLogEntryMethod = DeviceHandlerAuditLogger.class
+ .getDeclaredMethod("createAuditLogEntry", String.class, DeviceHandlerAuditLogger.Operation.class,
+ String.class);
+ createAuditLogEntryMethod.setAccessible(true);
+ JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke(
+ auditLogger, "testUserId", DeviceHandlerAuditLogger.Operation.UPDATE_DEVICE_MGT_CONFIG, null);
+
+ Assert.assertNotNull(result);
+ Assert.assertTrue(result.has("UpdatedAt"));
+ Assert.assertTrue(result.getLong("UpdatedAt") > 0);
+ Assert.assertTrue(result.has("DeviceId"));
+ Assert.assertTrue(result.isNull("DeviceId"));
+ Assert.assertFalse(result.has("RegisteredAt"));
+ Assert.assertFalse(result.has("UnregisteredAt"));
+ }
}
diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManagerTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManagerTest.java
new file mode 100644
index 0000000..d7cb8a3
--- /dev/null
+++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManagerTest.java
@@ -0,0 +1,615 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.notification.push.device.handler.utils;
+
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.MockitoAnnotations;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.wso2.carbon.identity.base.IdentityException;
+import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager;
+import org.wso2.carbon.identity.configuration.mgt.core.exception.ConfigurationManagementException;
+import org.wso2.carbon.identity.configuration.mgt.core.model.Attribute;
+import org.wso2.carbon.identity.configuration.mgt.core.model.Resource;
+import org.wso2.carbon.identity.configuration.mgt.core.model.ResourceTypeAdd;
+import org.wso2.carbon.identity.core.util.IdentityUtil;
+import org.wso2.carbon.identity.notification.push.device.handler.internal.PushDeviceHandlerDataHolder;
+import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationNotificationChannelEnum;
+import org.wso2.carbon.identity.notification.push.device.handler.model.PushDeviceMgtConfigData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_DOES_NOT_EXISTS;
+import static org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_MAX_DEVICE_LIMIT_PER_USER;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG;
+import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.MAX_DEVICE_LIMIT_PER_USER;
+
+/**
+ * Unit tests for PushDeviceConfigManager.
+ */
+public class PushDeviceConfigManagerTest {
+
+ private static final String TENANT_DOMAIN = "carbon.super";
+ private static final String RESOURCE_TYPE = "DEVICE_MANAGEMENT";
+ private static final String RESOURCE_NAME = "PUSH_DEVICE_MANAGEMENT";
+ private static final String ATTR_ENABLE_DEVICE_MANAGEMENT = "enableMultipleDeviceEnrollment";
+ private static final String ATTR_MAX_DEVICE_LIMIT = "maximumDeviceLimit";
+ private static final String DEFAULT_ENABLE_DEVICE_MANAGEMENT = "false";
+ private static final String DEFAULT_MAX_DEVICE_LIMIT = "2";
+
+ @Mock
+ private ConfigurationManager configurationManager;
+
+ private MockedStatic mockedDataHolder;
+ private MockedStatic mockedIdentityUtil;
+ private PushDeviceHandlerDataHolder dataHolderInstance;
+
+ @BeforeMethod
+ public void setUp() {
+
+ MockitoAnnotations.openMocks(this);
+ dataHolderInstance = mock(PushDeviceHandlerDataHolder.class);
+ mockedDataHolder = mockStatic(PushDeviceHandlerDataHolder.class);
+ mockedDataHolder.when(PushDeviceHandlerDataHolder::getInstance).thenReturn(dataHolderInstance);
+ when(dataHolderInstance.getConfigurationManager()).thenReturn(configurationManager);
+ mockedIdentityUtil = mockStatic(IdentityUtil.class);
+ mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER))
+ .thenReturn(null);
+ }
+
+ @AfterMethod
+ public void tearDown() {
+
+ mockedDataHolder.close();
+ mockedIdentityUtil.close();
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenResourceExists_ReturnsConfigData() throws Exception {
+
+ Resource resource = createResourceWithAttributes("true", "5");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ Assert.assertNotNull(result);
+ Assert.assertTrue(result.getEnableMultipleDeviceEnrollment(),
+ "Multiple device enrollment should be enabled as per the resource attributes.");
+ Assert.assertEquals(result.getMaximumDeviceLimit(), Integer.valueOf(5),
+ "Maximum device limit should match the resource attribute value.");
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenResourceNotFound_ReturnsDefaultConfig() throws Exception {
+
+ ConfigurationManagementException exception = mock(ConfigurationManagementException.class);
+ when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode());
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenThrow(exception);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ assertIsDefaultConfig(result);
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenResourceTypeNotFound_ReturnsDefaultConfig() throws Exception {
+
+ ConfigurationManagementException exception = mock(ConfigurationManagementException.class);
+ when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode());
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenThrow(exception);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ assertIsDefaultConfig(result);
+ }
+
+ @Test(expectedExceptions = IdentityException.class)
+ public void testGetPushDeviceConfig_WhenUnexpectedConfigException_ThrowsServerException() throws Exception {
+
+ ConfigurationManagementException exception = mock(ConfigurationManagementException.class);
+ when(exception.getErrorCode()).thenReturn("CONFIGM_UNEXPECTED_00099");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenThrow(exception);
+
+ PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenUnexpectedConfigException_ErrorCodeMatches() throws Exception {
+
+ ConfigurationManagementException exception = mock(ConfigurationManagementException.class);
+ when(exception.getErrorCode()).thenReturn("CONFIGM_UNEXPECTED_00099");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenThrow(exception);
+
+ try {
+ PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+ Assert.fail("Expected IdentityException was not thrown.");
+ } catch (IdentityException e) {
+ Assert.assertEquals(e.getErrorCode(), ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getCode(),
+ "Error code should be PDH-15014 for config retrieval failure.");
+ }
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenResourceIsNull_ReturnsDefaultConfig() throws Exception {
+
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(null);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ assertIsDefaultConfig(result);
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenResourceHasNullAttributes_ReturnsDefaultConfig() throws Exception {
+
+ Resource resource = new Resource();
+ resource.setResourceName(RESOURCE_NAME);
+ resource.setAttributes(null);
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ assertIsDefaultConfig(result);
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenResourceHasEmptyAttributes_ReturnsDefaultConfig() throws Exception {
+
+ Resource resource = new Resource();
+ resource.setResourceName(RESOURCE_NAME);
+ resource.setAttributes(new ArrayList<>());
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ assertIsDefaultConfig(result);
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_DefaultConfig_HasExpectedValues() throws Exception {
+
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(null);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.getEnableMultipleDeviceEnrollment(),
+ Boolean.valueOf(DEFAULT_ENABLE_DEVICE_MANAGEMENT),
+ "Default config must disable multiple device enrollment.");
+ Assert.assertEquals(result.getMaximumDeviceLimit(), Integer.valueOf(DEFAULT_MAX_DEVICE_LIMIT),
+ "Default config must set the maximum device limit to 1.");
+ Assert.assertFalse(result.getEnableDeviceRegistrationNotifications(),
+ "Default config must disable device registration notifications.");
+ Assert.assertEquals(result.getDeviceRegistrationNotificationChannels(),
+ java.util.EnumSet.of(DeviceRegistrationNotificationChannelEnum.EMAIL),
+ "Default config must default the notification channels to EMAIL only.");
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenResourceExists_CallsReplaceResource() throws Exception {
+
+ Resource existing = createResourceWithAttributes("false", "1");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = createConfigData(true, 5);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class));
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenResourceNotExists_CallsAddResource() throws Exception {
+
+ ConfigurationManagementException exception = mock(ConfigurationManagementException.class);
+ when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode());
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenThrow(exception);
+
+ PushDeviceMgtConfigData config = createConfigData(true, 3);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).addResource(eq(RESOURCE_TYPE), any(Resource.class));
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenResourceTypeNotExists_CreatesTypeAndAdds() throws Exception {
+
+ ConfigurationManagementException getException = mock(ConfigurationManagementException.class);
+ when(getException.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode());
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenThrow(getException);
+
+ ConfigurationManagementException addException = mock(ConfigurationManagementException.class);
+ when(addException.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode());
+ when(configurationManager.addResource(eq(RESOURCE_TYPE), any(Resource.class)))
+ .thenThrow(addException)
+ .thenReturn(new Resource());
+
+ PushDeviceMgtConfigData config = createConfigData(true, 2);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).addResourceType(any(ResourceTypeAdd.class));
+ }
+
+ @Test(expectedExceptions = IdentityException.class)
+ public void testUpdatePushDeviceConfig_WhenReplaceThrowsException_ThrowsServerException() throws Exception {
+
+ Resource existing = createResourceWithAttributes("false", "1");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ ConfigurationManagementException replaceException = mock(ConfigurationManagementException.class);
+ when(replaceException.getErrorCode()).thenReturn("CONFIGM_UNEXPECTED_00099");
+ when(configurationManager.replaceResource(eq(RESOURCE_TYPE), any(Resource.class)))
+ .thenThrow(replaceException);
+
+ PushDeviceMgtConfigData config = createConfigData(true, 5);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenReplaceThrowsException_ErrorCodeMatches() throws Exception {
+
+ Resource existing = createResourceWithAttributes("false", "1");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ ConfigurationManagementException replaceException = mock(ConfigurationManagementException.class);
+ when(replaceException.getErrorCode()).thenReturn("CONFIGM_UNEXPECTED_00099");
+ when(configurationManager.replaceResource(eq(RESOURCE_TYPE), any(Resource.class)))
+ .thenThrow(replaceException);
+
+ PushDeviceMgtConfigData config = createConfigData(true, 5);
+ try {
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+ Assert.fail("Expected IdentityException was not thrown.");
+ } catch (IdentityException e) {
+ Assert.assertEquals(e.getErrorCode(), ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG.getCode(),
+ "Error code should be PDH-15016 for config update failure.");
+ }
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_BuildsCorrectResourceAttributes() throws Exception {
+
+ Resource existing = createResourceWithAttributes("false", "1");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = createConfigData(true, 10);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class));
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenUpperBoundConfigIsInvalid_FallsBackToDefault() throws Exception {
+
+ mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER))
+ .thenReturn("not-a-number");
+ Resource existing = createResourceWithAttributes("true", "3");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = createConfigData(true, DEFAULT_MAX_DEVICE_LIMIT_PER_USER);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class));
+ }
+
+ @Test(expectedExceptions = IdentityException.class)
+ public void testUpdatePushDeviceConfig_WhenLimitExceedsServerUpperBound_ThrowsClientException()
+ throws Exception {
+
+ mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER))
+ .thenReturn("5");
+
+ PushDeviceMgtConfigData config = createConfigData(true, 6);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenLimitExceedsServerUpperBound_ErrorCodeMatches()
+ throws Exception {
+
+ mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER))
+ .thenReturn("5");
+
+ PushDeviceMgtConfigData config = createConfigData(true, 6);
+ try {
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+ Assert.fail("Expected IdentityException was not thrown.");
+ } catch (IdentityException e) {
+ Assert.assertEquals(e.getErrorCode(),
+ ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS.getCode(),
+ "Error code should be PDH-15018 when limit exceeds server upper bound.");
+ }
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenLimitEqualsServerUpperBound_Succeeds()
+ throws Exception {
+
+ mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER))
+ .thenReturn("5");
+ Resource existing = createResourceWithAttributes("true", "3");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = createConfigData(true, 5);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class));
+ }
+
+ @Test(expectedExceptions = IdentityException.class)
+ public void testUpdatePushDeviceConfig_WhenMultiDeviceDisabledAndLimitAboveOne_ThrowsClientException()
+ throws Exception {
+
+ Resource existing = createResourceWithAttributes("false", "3");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = createConfigData(false, 3);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenMultiDeviceDisabledAndLimitAboveOne_ErrorCodeMatches()
+ throws Exception {
+
+ Resource existing = createResourceWithAttributes("false", "3");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = createConfigData(false, 3);
+ try {
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+ Assert.fail("Expected IdentityException was not thrown.");
+ } catch (IdentityException e) {
+ Assert.assertEquals(e.getErrorCode(), ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG.getCode(),
+ "Error code should be PDH-15017 for invalid device limit config.");
+ }
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenMultiDeviceDisabledAndLimitIsOne_Succeeds() throws Exception {
+
+ ConfigurationManagementException exception = mock(ConfigurationManagementException.class);
+ when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode());
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenThrow(exception);
+
+ PushDeviceMgtConfigData config = createConfigData(false, 1);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).addResource(eq(RESOURCE_TYPE), any(Resource.class));
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenMultiDeviceEnabledAndLimitAboveOne_Succeeds() throws Exception {
+
+ Resource existing = createResourceWithAttributes("false", "1");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = createConfigData(true, 5);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class));
+ }
+
+ private Resource createResourceWithAttributes(String enableDeviceManagement, String maxDeviceLimit) {
+
+ Resource resource = new Resource();
+ resource.setResourceName(RESOURCE_NAME);
+ List attributes = new ArrayList<>();
+ attributes.add(new Attribute(ATTR_ENABLE_DEVICE_MANAGEMENT, enableDeviceManagement));
+ attributes.add(new Attribute(ATTR_MAX_DEVICE_LIMIT, maxDeviceLimit));
+ resource.setAttributes(attributes);
+ return resource;
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenAllAttributesPresent_MapsNotificationFlags() throws Exception {
+
+ Resource resource = createResourceWithAttributes("true", "5");
+ resource.getAttributes().add(new Attribute("enableDeviceRegistrationNotifications", "true"));
+ resource.getAttributes().add(new Attribute("deviceRegistrationNotificationChannels",
+ "EMAIL,PUSH_NOTIFICATION"));
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ Assert.assertTrue(result.getEnableDeviceRegistrationNotifications(),
+ "Notification enable flag should be mapped from the resource attributes.");
+ Assert.assertEquals(result.getDeviceRegistrationNotificationChannels(),
+ java.util.EnumSet.of(
+ DeviceRegistrationNotificationChannelEnum.EMAIL,
+ DeviceRegistrationNotificationChannelEnum.PUSH_NOTIFICATION),
+ "Notification channels should be mapped from the resource attributes.");
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenMaxDeviceLimitIsNull_SkipsLimitValidations() throws Exception {
+
+ Resource existing = createResourceWithAttributes("true", "3");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = new PushDeviceMgtConfigData();
+ config.setEnableMultipleDeviceEnrollment(false);
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class));
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenChannelsAttributeIsBlank_ReturnsEmptyChannelSet() throws Exception {
+
+ Resource resource = createResourceWithAttributes("true", "5");
+ resource.getAttributes().add(new Attribute("enableDeviceRegistrationNotifications", "true"));
+ resource.getAttributes().add(new Attribute("deviceRegistrationNotificationChannels", " "));
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ Assert.assertNotNull(result.getDeviceRegistrationNotificationChannels(),
+ "Channel set should not be null for blank attribute.");
+ Assert.assertTrue(result.getDeviceRegistrationNotificationChannels().isEmpty(),
+ "Channel set should be empty when the attribute is blank.");
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenChannelsAttributeHasInvalidAndEmptyTokens_SkipsThem() throws Exception {
+
+ Resource resource = createResourceWithAttributes("true", "5");
+ resource.getAttributes().add(new Attribute("enableDeviceRegistrationNotifications", "true"));
+ resource.getAttributes().add(new Attribute("deviceRegistrationNotificationChannels",
+ "EMAIL, ,UNKNOWN_CHANNEL,push_notification"));
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource);
+
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ Assert.assertEquals(result.getDeviceRegistrationNotificationChannels(),
+ java.util.EnumSet.of(
+ DeviceRegistrationNotificationChannelEnum.EMAIL,
+ DeviceRegistrationNotificationChannelEnum.PUSH_NOTIFICATION),
+ "Empty and unknown tokens should be skipped while keeping valid channels.");
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WithNotificationChannels_SerialisesIntoAttribute() throws Exception {
+
+ Resource existing = createResourceWithAttributes("true", "3");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing);
+
+ PushDeviceMgtConfigData config = createConfigData(true, 3);
+ config.setEnableDeviceRegistrationNotifications(true);
+ config.setDeviceRegistrationNotificationChannels(java.util.EnumSet.of(
+ DeviceRegistrationNotificationChannelEnum.EMAIL,
+ DeviceRegistrationNotificationChannelEnum.PUSH_NOTIFICATION));
+
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Resource.class);
+ verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), captor.capture());
+
+ String serialised = captor.getValue().getAttributes().stream()
+ .filter(a -> "deviceRegistrationNotificationChannels".equals(a.getKey()))
+ .map(Attribute::getValue)
+ .findFirst()
+ .orElse(null);
+ Assert.assertNotNull(serialised, "Notification channels attribute should be present.");
+ java.util.Set tokens = new java.util.HashSet<>(java.util.Arrays.asList(serialised.split(",")));
+ Assert.assertEquals(tokens,
+ new java.util.HashSet<>(java.util.Arrays.asList("EMAIL", "PUSH_NOTIFICATION")),
+ "Serialised channels should contain EMAIL and PUSH_NOTIFICATION tokens.");
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenMultiDeviceEnabledAndLimitIsNull_ThrowsClientException()
+ throws Exception {
+
+ PushDeviceMgtConfigData config = new PushDeviceMgtConfigData();
+ config.setEnableMultipleDeviceEnrollment(true);
+ // maximumDeviceLimit intentionally left null.
+ try {
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+ Assert.fail("Expected IdentityException was not thrown.");
+ } catch (IdentityException e) {
+ Assert.assertEquals(e.getErrorCode(), ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE.getCode(),
+ "Error code should be PDH-15021 when multi-device is enabled without a positive limit.");
+ }
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenMultiDeviceEnabledAndLimitIsNotPositive_ThrowsClientException()
+ throws Exception {
+
+ PushDeviceMgtConfigData config = createConfigData(true, 0);
+ try {
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+ Assert.fail("Expected IdentityException was not thrown.");
+ } catch (IdentityException e) {
+ Assert.assertEquals(e.getErrorCode(), ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE.getCode(),
+ "Error code should be PDH-15021 when the limit is not positive.");
+ }
+ }
+
+ @Test
+ public void testUpdatePushDeviceConfig_WhenMultiDeviceDisabledAndLimitIsNull_PersistsDefaultLimit()
+ throws Exception {
+
+ ConfigurationManagementException exception = mock(ConfigurationManagementException.class);
+ when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode());
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenThrow(exception);
+
+ PushDeviceMgtConfigData config = new PushDeviceMgtConfigData();
+ config.setEnableMultipleDeviceEnrollment(false);
+ // maximumDeviceLimit intentionally left null.
+ PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN);
+
+ org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Resource.class);
+ verify(configurationManager).addResource(eq(RESOURCE_TYPE), captor.capture());
+
+ String persistedLimit = captor.getValue().getAttributes().stream()
+ .filter(a -> ATTR_MAX_DEVICE_LIMIT.equals(a.getKey()))
+ .map(Attribute::getValue)
+ .findFirst()
+ .orElse(null);
+ // A null limit must never be persisted as the literal string "null".
+ Assert.assertEquals(persistedLimit, DEFAULT_MAX_DEVICE_LIMIT,
+ "A null maximum device limit must be normalised to the default before persistence.");
+ }
+
+ @Test
+ public void testGetPushDeviceConfig_WhenStoredLimitIsNotNumeric_FallsBackToDefault() throws Exception {
+
+ Resource resource = createResourceWithAttributes("true", "null");
+ when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource);
+
+ // A corrupted/legacy non-numeric limit must not make the config unreadable.
+ PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN);
+
+ Assert.assertEquals(result.getMaximumDeviceLimit(), Integer.valueOf(DEFAULT_MAX_DEVICE_LIMIT),
+ "A non-numeric stored limit must fall back to the default.");
+ }
+
+ private PushDeviceMgtConfigData createConfigData(boolean enableMultipleDeviceEnrollment, int maximumDeviceLimit) {
+
+ PushDeviceMgtConfigData config = new PushDeviceMgtConfigData();
+ config.setEnableMultipleDeviceEnrollment(enableMultipleDeviceEnrollment);
+ config.setMaximumDeviceLimit(maximumDeviceLimit);
+ return config;
+ }
+
+ private void assertIsDefaultConfig(PushDeviceMgtConfigData config) {
+
+ Assert.assertNotNull(config, "Default config must not be null.");
+ Assert.assertEquals(config.getEnableMultipleDeviceEnrollment(),
+ Boolean.valueOf(DEFAULT_ENABLE_DEVICE_MANAGEMENT),
+ "Default config must disable multiple device enrollment.");
+ Assert.assertEquals(config.getMaximumDeviceLimit(), Integer.valueOf(DEFAULT_MAX_DEVICE_LIMIT),
+ "Default config must set the maximum device limit to 1.");
+ }
+}
diff --git a/components/org.wso2.carbon.identity.notification.push.provider/src/main/java/org/wso2/carbon/identity/notification/push/provider/model/PushNotificationData.java b/components/org.wso2.carbon.identity.notification.push.provider/src/main/java/org/wso2/carbon/identity/notification/push/provider/model/PushNotificationData.java
index 619dbc7..d24654f 100644
--- a/components/org.wso2.carbon.identity.notification.push.provider/src/main/java/org/wso2/carbon/identity/notification/push/provider/model/PushNotificationData.java
+++ b/components/org.wso2.carbon.identity.notification.push.provider/src/main/java/org/wso2/carbon/identity/notification/push/provider/model/PushNotificationData.java
@@ -47,6 +47,9 @@ public class PushNotificationData {
private final String ipAddress;
private final String deviceOS;
private final String browser;
+ private final String pushDeviceName;
+ private final String pushDeviceModel;
+ private final String registrationTime;
private PushNotificationData(Builder builder) {
@@ -69,6 +72,9 @@ private PushNotificationData(Builder builder) {
this.ipAddress = builder.ipAddress;
this.deviceOS = builder.deviceOS;
this.browser = builder.browser;
+ this.pushDeviceName = builder.pushDeviceName;
+ this.pushDeviceModel = builder.pushDeviceModel;
+ this.registrationTime = builder.registrationTime;
}
public String getNotificationTitle() {
@@ -161,6 +167,21 @@ public String getDeviceOS() {
return deviceOS;
}
+ public String getPushDeviceName() {
+
+ return pushDeviceName;
+ }
+
+ public String getPushDeviceModel() {
+
+ return pushDeviceModel;
+ }
+
+ public String getRegistrationTime() {
+
+ return registrationTime;
+ }
+
public Map getAdditionalData() {
Map additionalData = new HashMap<>();
@@ -211,6 +232,15 @@ public Map getAdditionalData() {
if (deviceId != null) {
additionalData.put("deviceId", deviceId);
}
+ if (pushDeviceName != null) {
+ additionalData.put("push-device-name", pushDeviceName);
+ }
+ if (pushDeviceModel != null) {
+ additionalData.put("push-device-model", pushDeviceModel);
+ }
+ if (registrationTime != null) {
+ additionalData.put("registration-time", registrationTime);
+ }
return additionalData;
}
@@ -250,6 +280,9 @@ public static class Builder {
private String ipAddress;
private String deviceOS;
private String browser;
+ private String pushDeviceName;
+ private String pushDeviceModel;
+ private String registrationTime;
public Builder setNotificationTitle(String notificationTitle) {
@@ -365,6 +398,24 @@ public Builder setBrowser(String browser) {
return this;
}
+ public Builder setPushDeviceName(String pushDeviceName) {
+
+ this.pushDeviceName = pushDeviceName;
+ return this;
+ }
+
+ public Builder setPushDeviceModel(String pushDeviceModel) {
+
+ this.pushDeviceModel = pushDeviceModel;
+ return this;
+ }
+
+ public Builder setRegistrationTime(String registrationTime) {
+
+ this.registrationTime = registrationTime;
+ return this;
+ }
+
public PushNotificationData build() {
return new PushNotificationData(this);
diff --git a/pom.xml b/pom.xml
index ba1e571..5bfc2ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -83,10 +83,15 @@
org.wso2.carbon.database.utils
${org.wso2.carbon.database.utils.version}
+
+ org.wso2.carbon.identity.event.handler.notification
+ org.wso2.carbon.identity.event.handler.notification
+ ${identity.event.handler.notification.version}
+
org.wso2.carbon.identity.event.handler.notification
org.wso2.carbon.identity.notification.sender.tenant.config
- ${identity.notification.sender.tenant.config.version}
+ ${identity.event.handler.notification.version}
org.wso2.orbit.com.nimbusds
@@ -124,6 +129,11 @@
org.wso2.carbon.identity.secret.mgt.core
${carbon.identity.framework.version}
+
+ org.wso2.carbon.identity.framework
+ org.wso2.carbon.identity.configuration.mgt.core
+ ${carbon.identity.framework.version}
+
org.json.wso2
json
@@ -218,6 +228,11 @@
${com.fasterxml.jackson.databind.version}
test
+
+ org.wso2.carbon.identity.framework
+ org.wso2.carbon.identity.event
+ ${carbon.identity.framework.version}
+
@@ -352,7 +367,7 @@
- 7.8.635
+ 7.11.159
[7.5.0, 8.0.0)
1.1.15
[1.0.0, 2.0.0)
@@ -361,8 +376,9 @@
[4.5.0, 5.0.0)
2.1.6
[2.0.0,3.0.0)
- 1.12.4
+ 1.13.7
[1.3.15, 2.0.0)
+ [1.0.0, 2.0.0)
1.20.0.wso2v1
1.43.3.wso2v1
[1.41.2,1.44.0)