diff --git a/distributor/src/distributor/messenger.bal b/distributor/src/distributor/messenger.bal index 72c43ac..0dde5ac 100644 --- a/distributor/src/distributor/messenger.bal +++ b/distributor/src/distributor/messenger.bal @@ -1,4 +1,5 @@ import ballerina/config; + import ballerina/io; import ballerina/log; import ballerina/stringutils; import chamil/govsms; @@ -85,8 +86,9 @@ function validate(string mobileNo) returns string|error { boolean number = stringutils:matches(mobile, "^[0-9]*$"); if !number { - return error(ERROR_REASON, message = "Invalid mobile number. Given mobile number contains non numeric " + - "characters: " + mobile); + string errorMsg = "Invalid mobile number. Given mobile number contains non numeric characters: " + mobile; + log:printError(errorMsg); + return error(ERROR_REASON, message = errorMsg); } if (mobile.startsWith("0") && mobile.length() == 10) { @@ -96,6 +98,7 @@ function validate(string mobileNo) returns string|error { return mobile; } // Allow only the local mobile numbers to register via public API. International number are avoided. + log:printError("Invalid mobile number : " + mobile); return error(ERROR_REASON, message = "Invalid mobile number. Resend the request as follows: If the " + "mobile no is 0771234567, send POST request to \'/sms\' with JSON payload " + "\'{\"username\":\"myuser\", \"mobile\":\"0771234567\"}\'"); @@ -106,7 +109,7 @@ function validate(string mobileNo) returns string|error { # + username - The recipient username # + mobileNo - The recipient number # + return - The status of registration or operation error -function registerAsSMSRecipient(string username, string mobileNo) returns string|error { +function registerSmsRecipient(string username, string mobileNo) returns string|error { if mobileSubscribers.hasKey(username) { string errMsg = "Registration failed: username:" + username + " is already registered with mobile:" + mobileNo; @@ -131,7 +134,7 @@ function registerAsSMSRecipient(string username, string mobileNo) returns string # + username - The recipient username # + mobileNo - The recipient number # + return - The status of deregistration or operation error -function unregisterAsSMSRecipient(string username, string mobileNo) returns string|error { +function unregisterSmsRecipient(string username, string mobileNo) returns string|error { string result = ""; if mobileSubscribers.hasKey(username) && mobileSubscribers.get(username) == mobileNo { result = "Successfully unregistered: username:" + username + " mobile:" + mobileSubscribers.remove(username); @@ -150,3 +153,69 @@ function unregisterAsSMSRecipient(string username, string mobileNo) returns stri } return result; } + +function unregisterAllSMSRecipient() returns error? { + mobileSubscribers = {}; + _ = check dbClient->update(DELETE_RECIPIENT_TABLE); +} + +function readRecipients(io:ReadableByteChannel|error payload) returns @tainted Recipient[]|error { + + io:ReadableByteChannel rbChannel = check payload; + + io:ReadableCharacterChannel rch = new (rbChannel, "UTF8"); + var result = rch.readJson(); + closeReadableChannel(rch); + if (result is error) { + string logMessage = "Error occurred while reading json: malformed Recipient[]"; + return error(ERROR_REASON, message = logMessage, cause = result); + } + + Recipient[]|error recipientArray = Recipient[].constructFrom(result); + if recipientArray is error { + string logMessage = "Error occurred while converting json: malformed Recipient[]"; + return error(ERROR_REASON, message = logMessage, cause = recipientArray); + } + return recipientArray; +} + +function closeReadableChannel(io:ReadableCharacterChannel rc) { + var result = rc.close(); + if (result is error) { + log:printError("Error occurred while closing character stream", result); + } +} + +function validateAllRecipients(Recipient[] recipients) returns error? { + string errMsg = "invalid recipient mobile no: "; + boolean erroneous = false; + foreach var user in recipients { + string|error validatedNo = validate(user.mobile); + if validatedNo is error { + erroneous = true; + errMsg = errMsg + user.username + ":" + user.mobile + ", "; + } else { + user.mobile = validatedNo; + } + } + if erroneous { + return error(ERROR_REASON, message = errMsg.substring(0, errMsg.length() - 2)); + } +} + +function registerAllSMSRecipients(Recipient[] recipients) returns error? { + string errMsg = ""; + boolean erroneous = false; + foreach var user in recipients { + string|error status = registerSmsRecipient(user.username.trim(), user.mobile); + if status is error { + erroneous = true; + errMsg = errMsg + user.username + ":" + user.mobile + ", "; + } else { + log:printInfo(status); + } + } + if erroneous { + return error(ERROR_REASON, message = errMsg.substring(0, errMsg.length() - 2)); + } +} diff --git a/distributor/src/distributor/save.bal b/distributor/src/distributor/save.bal index 4f6fbeb..07fd7e9 100644 --- a/distributor/src/distributor/save.bal +++ b/distributor/src/distributor/save.bal @@ -45,6 +45,7 @@ const INSERT_RECIPIENT = "INSERT INTO smsRecipients (username, mobileNo) VALUES const DELETE_RECIPIENT = "DELETE FROM smsRecipients WHERE username = ?"; const SELECT_RECIPIENT_DATA = "SELECT * FROM smsRecipients"; const DROP_RECIPIENT_TABLE = "DROP TABLE smsRecipients"; +const DELETE_RECIPIENT_TABLE = "DELETE FROM smsRecipients"; jdbc:Client dbClient = new ({ url: config:getAsString("eclk.hub.db.url"), diff --git a/distributor/src/distributor/tests/resources/contact1.json b/distributor/src/distributor/tests/resources/contact1.json new file mode 100644 index 0000000..51d0b23 --- /dev/null +++ b/distributor/src/distributor/tests/resources/contact1.json @@ -0,0 +1,10 @@ +[ + { + "username":"newuser1", + "mobile":"0711234561" + }, + { + "username":"newuser2", + "mobile":"0711234562" + } +] diff --git a/distributor/src/distributor/tests/resources/contact2.json b/distributor/src/distributor/tests/resources/contact2.json new file mode 100644 index 0000000..28c18aa --- /dev/null +++ b/distributor/src/distributor/tests/resources/contact2.json @@ -0,0 +1,10 @@ +[ + { + "username":"newuser1", + "mobile":"+00771234562" + }, + { + "username":"newuser2", + "mobile":"0711234562" + } +] diff --git a/distributor/src/distributor/tests/resources/contact3.json b/distributor/src/distributor/tests/resources/contact3.json new file mode 100644 index 0000000..01ff1e8 --- /dev/null +++ b/distributor/src/distributor/tests/resources/contact3.json @@ -0,0 +1,10 @@ +[ + { + "user":"newuser1", + "mobi":"0771234562" + }, + { + "usrname":"newuser2", + "mobie":"0711234562" + } +] diff --git a/distributor/src/distributor/tests/sms_publisher_test.bal b/distributor/src/distributor/tests/sms_publisher_test.bal index 35371ea..c2c9429 100644 --- a/distributor/src/distributor/tests/sms_publisher_test.bal +++ b/distributor/src/distributor/tests/sms_publisher_test.bal @@ -9,7 +9,7 @@ import ballerina/test; // [b7a.users.test] // password="password" // scopes="ECAdmin" -@test:Config { enable: false } +@test:Config { enable: false, after: "testResetRecipients" } function testSubscriberRegistration() { auth:OutboundBasicAuthProvider outboundBasicAuthProvider1 = new({ username: "test", @@ -96,6 +96,77 @@ function testSubscriberRegistration() { } else { test:assertFail(msg = "Failed to call the endpoint:"); } + + // Test bulk success registration + req = new; + req.setFileAsPayload("src/distributor/tests/resources/contact1.json"); + response = httpEndpoint->post("/sms/addall", req); + if (response is http:Response) { + var result = response.getTextPayload(); + if (result is string) { + test:assertEquals(result, "Successfully registered all"); + } else { + test:assertFail(msg = "Invalid response message:"); + } + } else { + test:assertFail(msg = "Failed to call the endpoint:"); + } + + // Test bulk registration with invalid nos + req = new; + req.setFileAsPayload("src/distributor/tests/resources/contact2.json"); + response = httpEndpoint->post("/sms/addall", req); + if (response is http:Response) { + var result = response.getTextPayload(); + if (result is string) { + test:assertEquals(result, "Validation failed: invalid recipient mobile no: newuser1:+00771234562"); + } else { + test:assertFail(msg = "Invalid response message:"); + } + } else { + test:assertFail(msg = "Failed to call the endpoint:"); + } + + // Test bulk registration with malformed JSON + req = new; + req.setFileAsPayload("src/distributor/tests/resources/contact3.json"); + response = httpEndpoint->post("/sms/addall", req); + if (response is http:Response) { + var result = response.getTextPayload(); + if (result is string) { + test:assertTrue(stringutils:contains(result, "Error occurred while converting json: malformed Recipient")); + } else { + test:assertFail(msg = "Invalid response message:"); + } + } else { + test:assertFail(msg = "Failed to call the endpoint:"); + } +} + +@test:Config { before: "testSubscriberRegistration" } +function testResetRecipients() { + auth:OutboundBasicAuthProvider outboundBasicAuthProvider1 = new({ + username: "test", + password: config:getAsString("b7a.users.test.password") + }); + http:BasicAuthHandler outboundBasicAuthHandler1 = new(outboundBasicAuthProvider1); + http:Client httpEndpoint = new("http://localhost:9090", { + auth: { + authHandler: outboundBasicAuthHandler1 + } + }); + http:Request req = new; + var response = httpEndpoint->delete("/sms/reset", req); + if (response is http:Response) { + var result = response.getTextPayload(); + if (result is string) { + test:assertEquals(result, "Successfully unregistered all"); + } else { + test:assertFail(msg = "Invalid response message:"); + } + } else { + test:assertFail(msg = "Failed to call the endpoint:"); + } } @test:Config {} diff --git a/distributor/src/distributor/website.bal b/distributor/src/distributor/website.bal index efe1138..bd30d12 100644 --- a/distributor/src/distributor/website.bal +++ b/distributor/src/distributor/website.bal @@ -206,13 +206,48 @@ service mediaWebsite on mediaListener { } // If the load is high, we might need to sync following db/map update - string|error status = registerAsSMSRecipient(smsRecipient.username.trim(), validatedNo); + string|error status = registerSmsRecipient(smsRecipient.username.trim(), validatedNo); if status is error { return caller->internalServerError(<@untainted> status.detail()?.message); } return caller->ok(<@untainted> status); } + // This API enables registering mobile nos via a file(binary payload). Make sure the data is structured + // as an array of Recipients and the content type is `application/octet-stream`. + // [ + // { "username":"newuser1", "mobile":"0771234567" }, + // { "username":"newuser2", "mobile":"0771234568" }, + // { "username":"newuser3", "mobile":"0771234569" } + // ] + @http:ResourceConfig { + path: "/sms/addall", + methods: ["POST"], + consumes: ["application/octet-stream"], + auth: { + scopes: ["ECAdmin"] + } + } + resource function smsBulkRegistration (http:Caller caller, http:Request req) returns error? { + Recipient[]|error recipient = readRecipients(req.getByteChannel()); + if recipient is error { + log:printError("Invalid input", recipient); + return caller->badRequest(<@untainted> recipient.toString()); + } + Recipient[] smsRecipient = recipient; + error? validatedNo = validateAllRecipients(smsRecipient); + if validatedNo is error { + return caller->badRequest("Validation failed: " + <@untainted string> validatedNo.detail()?.message); + } + + error? status = registerAllSMSRecipients(smsRecipient); + if status is error { + return caller->internalServerError("Registration failed: " + <@untainted string> status.detail()?.message); + } + + return caller->ok("Successfully registered all"); + } + @http:ResourceConfig { path: "/sms", methods: ["DELETE"], @@ -228,13 +263,28 @@ service mediaWebsite on mediaListener { } // If the load is high, we might need to sync following db/map update - string|error status = unregisterAsSMSRecipient(smsRecipient.username.trim(), validatedNo); + string|error status = unregisterSmsRecipient(smsRecipient.username.trim(), validatedNo); if status is error { return caller->internalServerError(<@untainted string> status.detail()?.message); } return caller->ok(<@untainted string> status); } + @http:ResourceConfig { + path: "/sms/reset", + methods: ["DELETE"], + auth: { + scopes: ["ECAdmin"] + } + } + resource function resetSmsRecipients(http:Caller caller, http:Request req) returns error? { + error? status = unregisterAllSMSRecipient(); + if status is error { + return caller->internalServerError( status.detail()?.message); + } + return caller->ok("Successfully unregistered all"); + } + // May have to move to a separate service. @http:ResourceConfig { webSocketUpgrade: {