-
Notifications
You must be signed in to change notification settings - Fork 8
feat: implement device registration API with kafka event publishing #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
0bcc8e2
c0a657f
507d9b4
003cb05
262b62b
3b19b45
03cce7a
65030bf
f7da86c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package validators; | ||
|
|
||
| import org.junit.Assert; | ||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
| import org.sunbird.exception.ProjectCommonException; | ||
| import org.sunbird.message.ResponseCode; | ||
| import org.sunbird.request.Request; | ||
|
|
||
| /** | ||
| * Unit tests for DeviceRegisterRequestValidator. | ||
| */ | ||
| public class DeviceRegisterRequestValidatorTest { | ||
|
|
||
| private DeviceRegisterRequestValidator validator; | ||
| private Request request; | ||
|
|
||
| @Before | ||
| public void setUp() { | ||
| validator = new DeviceRegisterRequestValidator(); | ||
| request = new Request(); | ||
| request.setRequest(new java.util.HashMap<>()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidate_throwsWhenDeviceIdMissing() { | ||
| // Arrange | ||
| request.getRequest().put("deviceId", null); | ||
|
|
||
| // Act & Assert | ||
| try { | ||
| validator.validate(request); | ||
| Assert.fail("Expected ProjectCommonException"); | ||
| } catch (ProjectCommonException ex) { | ||
| Assert.assertEquals(ResponseCode.mandatoryParameterMissing.getErrorCode(), ex.getErrorCode()); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidate_throwsWhenDeviceIdBlank() { | ||
| // Arrange | ||
| request.getRequest().put("deviceId", ""); | ||
|
|
||
| // Act & Assert | ||
| try { | ||
| validator.validate(request); | ||
| Assert.fail("Expected ProjectCommonException"); | ||
| } catch (ProjectCommonException ex) { | ||
| Assert.assertEquals(ResponseCode.mandatoryParameterMissing.getErrorCode(), ex.getErrorCode()); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidate_passesWithOnlyDeviceId() { | ||
| // Arrange | ||
| request.getRequest().put("deviceId", "test-device-001"); | ||
|
|
||
| // Act & Assert | ||
| validator.validate(request); // Should not throw | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidate_passesWithFullRequest() { | ||
| // Arrange | ||
| request.getRequest().put("deviceId", "test-device-001"); | ||
| request.getRequest().put("fcmToken", "token123"); | ||
| request.getRequest().put("producer", "test-app"); | ||
|
|
||
| // Act & Assert | ||
| validator.validate(request); // Should not throw | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package controllers.device; | ||
|
|
||
| import controllers.BaseController; | ||
| import java.util.concurrent.CompletionStage; | ||
| import javax.inject.Inject; | ||
| import javax.inject.Named; | ||
| import org.apache.pekko.actor.ActorRef; | ||
| import org.sunbird.request.Request; | ||
| import play.mvc.Http; | ||
| import play.mvc.Result; | ||
| import validators.DeviceRegisterRequestValidator; | ||
|
|
||
| /** | ||
| * REST controller for device registration endpoint. | ||
| * Handles POST /v1/device/register/:deviceId requests. | ||
| */ | ||
| public class DeviceController extends BaseController { | ||
|
|
||
| @Inject | ||
| @Named("device_register_actor") private ActorRef deviceRegisterActorRef; | ||
|
|
||
| /** | ||
| * Register or update a device profile. | ||
| * Extracts path param (deviceId) and headers (IP, User-Agent). | ||
| * Delegates to Pekko actor via ask pattern. | ||
| * | ||
| * @param deviceId Unique device identifier (path param) | ||
| * @param httpRequest HTTP request with body, headers | ||
| * @return Async result (success or error response) | ||
| */ | ||
| public CompletionStage<Result> registerDevice(String deviceId, Http.Request httpRequest) { | ||
| return handleRequest( | ||
| deviceRegisterActorRef, | ||
| "registerDevice", | ||
| httpRequest.body().asJson(), | ||
| req -> { | ||
| Request request = (Request) req; | ||
| // Inject deviceId and headers into request | ||
| request.getRequest().put("deviceId", deviceId); | ||
| request.getRequest().put("ip_addr", resolveIp(httpRequest)); | ||
| request.getRequest().put("user_agent", httpRequest.header("User-Agent").orElse("")); | ||
| // Validate | ||
| new DeviceRegisterRequestValidator().validate(request); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit — validator not injected Every other controller in this project injects its validator (e.g. via Consider: @Inject private DeviceRegisterRequestValidator validator;
// ...
validator.validate(request); |
||
| return null; | ||
| }, | ||
| httpRequest); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve client IP from headers. | ||
| * X-Real-IP (set by load balancer) takes precedence over remote address. | ||
| * | ||
| * @param req HTTP request | ||
| * @return Client IP or empty string | ||
| */ | ||
| private String resolveIp(Http.Request req) { | ||
| return req.header("X-Real-IP").orElse(req.header("X-Forwarded-For").orElse("")); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security — IP header spoofing
The safe pattern depends on your infra:
Suggestion: add a comment naming the LB/proxy that sets this header, or use |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package validators; | ||
|
|
||
| import java.util.Map; | ||
| import org.apache.commons.lang3.StringUtils; | ||
| import org.sunbird.exception.ProjectCommonException; | ||
| import org.sunbird.message.ResponseCode; | ||
| import org.sunbird.request.Request; | ||
|
|
||
| /** | ||
| * Validator for device registration requests. | ||
| */ | ||
| public class DeviceRegisterRequestValidator { | ||
|
|
||
| /** | ||
| * Validates device registration request. | ||
| * Required: deviceId (non-blank). | ||
| * Optional: fcmToken, producer, dspec, userDeclaredLocation, etc. | ||
| * | ||
| * @param request The request to validate | ||
| * @throws ProjectCommonException if validation fails | ||
| */ | ||
| public void validate(Request request) { | ||
| Map<String, Object> req = request.getRequest(); | ||
|
|
||
| String deviceId = (String) req.get("deviceId"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — potential If the JSON body contains Object raw = req.get("deviceId");
String deviceId = (raw != null) ? raw.toString() : null; |
||
| if (StringUtils.isBlank(deviceId)) { | ||
| throw new ProjectCommonException( | ||
| ResponseCode.mandatoryParameterMissing.getErrorCode(), | ||
| ResponseCode.mandatoryParameterMissing.getErrorMessage() + " deviceId", | ||
| ResponseCode.CLIENT_ERROR.getResponseCode()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package org.sunbird.actor.device; | ||
|
|
||
| import org.sunbird.actor.core.BaseActor; | ||
| import org.sunbird.request.Request; | ||
| import org.sunbird.response.Response; | ||
| import org.sunbird.service.device.DeviceRegisterService; | ||
| import org.sunbird.service.device.impl.DeviceRegisterServiceImpl; | ||
| import org.sunbird.telemetry.dto.TelemetryEnvKey; | ||
| import org.sunbird.util.Util; | ||
|
|
||
| /** | ||
| * Pekko actor for device registration and FCM token management. | ||
| * | ||
| * <p>Handles asynchronous device registration requests and delegates to the service layer for | ||
| * persistence and event publishing. Each device registration updates or creates a device profile | ||
| * in YugabyteSQL and publishes a notification event to Kafka for downstream processing. | ||
| * | ||
| * <p>Supported operations: | ||
| * <ul> | ||
| * <li>{@code registerDevice} - Register or update a device profile with FCM token and metadata. | ||
| * </ul> | ||
| * | ||
| * @see DeviceRegisterService | ||
| * @see DeviceRegisterServiceImpl | ||
| */ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — field should be
private final DeviceRegisterService deviceRegisterService; |
||
| public class DeviceRegisterActor extends BaseActor { | ||
|
|
||
| private DeviceRegisterService deviceRegisterService; | ||
|
|
||
| /** | ||
| * Constructs a DeviceRegisterActor with default DeviceRegisterServiceImpl. | ||
| * Used in production when the actor is instantiated by the Pekko framework. | ||
| */ | ||
| public DeviceRegisterActor() { | ||
| this.deviceRegisterService = new DeviceRegisterServiceImpl(); | ||
| } | ||
|
|
||
| /** | ||
| * Constructs a DeviceRegisterActor with a provided service for dependency injection. | ||
| * Used in testing to mock the service layer. | ||
| * | ||
| * @param service the DeviceRegisterService implementation to use | ||
| */ | ||
| DeviceRegisterActor(DeviceRegisterService service) { | ||
| this.deviceRegisterService = service; | ||
| } | ||
|
|
||
| /** | ||
| * Receives and routes incoming device registration requests. | ||
| * | ||
| * <p>Initializes request context with telemetry metadata, routes the operation to the | ||
| * appropriate handler, and sends the response back to the sender. | ||
| * | ||
| * @param request the incoming request containing operation type and device data | ||
| * @throws Throwable if an error occurs during request processing | ||
| */ | ||
| @Override | ||
| public void onReceive(Request request) throws Throwable { | ||
| Util.initializeContext(request, TelemetryEnvKey.USER); | ||
| String operation = request.getOperation(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — wrong telemetry env key
|
||
|
|
||
| switch (operation) { | ||
| case "registerDevice": | ||
| registerDevice(request); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — hard-coded operation string Other actors in this project route on case ActorOperations.REGISTER_DEVICE.getValue():This also makes it easier to |
||
| break; | ||
| default: | ||
| onReceiveUnsupportedOperation(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Registers or updates a device profile with FCM token. | ||
| * | ||
| * <p>Delegates to the service layer to: | ||
| * <ul> | ||
| * <li>Validate device metadata (deviceId, ip_addr, user_agent, fcmToken) | ||
| * <li>Upsert device profile in YugabyteSQL | ||
| * <li>Publish device registration event to Kafka | ||
| * </ul> | ||
| * | ||
| * The response is sent back to the sender asynchronously. | ||
| * | ||
| * @param request the device registration request containing deviceId and profile data | ||
| * @throws Throwable if service layer processing fails | ||
| */ | ||
| private void registerDevice(Request request) throws Throwable { | ||
| logger.info(request.getRequestContext(), "DeviceRegisterActor:registerDevice: method called."); | ||
| Response response = deviceRegisterService.registerDevice(request); | ||
| sender().tell(response, self()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package org.sunbird.dao.device; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Data Access Object interface for device profiles. | ||
| * Handles UPSERT operations to YugabyteSQL database. | ||
| */ | ||
| public interface DeviceProfileDao { | ||
|
|
||
| /** | ||
| * UPSERT device profile to database. | ||
| * On conflict (device_id exists): updates all fields except first_access (preserved). | ||
| * | ||
| * @param profile Map containing device profile fields | ||
| * @throws Exception if database operation fails | ||
| */ | ||
| void upsert(Map<String, Object> profile) throws Exception; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit — verify dispatcher reference
The pool entry uses
dispatcher = rr-dispatcher, but the child dispatcher line usespekko.actor.rr-dispatcher(with thepekko.actor.prefix). Compare with the other actor blocks immediately above where child dispatchers are referenced aspekko.actor.most-used-one-dispatcher. Therr-dispatcheralias should resolve to a defined dispatcher — double-check this doesn't silently fall back to the default dispatcher in production.