From fc52cc3b274961ba2860e314b3b2e59d0e82e1ee Mon Sep 17 00:00:00 2001 From: Thomas Ryan Date: Thu, 18 Apr 2024 21:34:41 -0700 Subject: [PATCH] More work in support of #438. --- Messaging.Tests/UnitTest1.cs | 30 +++-- ...0418224229_RegistrationTesting.Designer.cs | 112 +++++++++++++++++ .../20240418224229_RegistrationTesting.cs | 30 +++++ .../MessagingContextModelSnapshot.cs | 5 +- Messaging/Program.cs | 114 +++++++++++++----- .../NumberSearch.DataAccess.csproj | 4 +- NumberSearch.Ops/NumberSearch.Ops.csproj | 2 +- 7 files changed, 254 insertions(+), 43 deletions(-) create mode 100644 Messaging/Migrations/20240418224229_RegistrationTesting.Designer.cs create mode 100644 Messaging/Migrations/20240418224229_RegistrationTesting.cs diff --git a/Messaging.Tests/UnitTest1.cs b/Messaging.Tests/UnitTest1.cs index fbd2b950..b0c6ea1c 100644 --- a/Messaging.Tests/UnitTest1.cs +++ b/Messaging.Tests/UnitTest1.cs @@ -104,6 +104,15 @@ public async Task RegisterAClientAsync() Assert.Equal("https://sms.callpipe.com/swagger/index.html", data.CallbackUrl); } + // TODO: A Complete functional Test includes these steps: + // 1. Login + // 2. Register a Client + // 3. Verify Routing + // 4. Send Outbound SMS + // 5. Recieve Inbound SMS + // 6. Send Outbound MMS + // 7. Recieve Inbound MMS + [Fact] public async Task GetAllClientsAsync() { @@ -130,12 +139,11 @@ public async Task CorrectlyFormattedButInvalidMessage() string route = "/api/inbound/1pcom"; string token = "okereeduePeiquah3yaemohGhae0ie"; - var stringContent = new FormUrlEncodedContent(new[] - { + var stringContent = new FormUrlEncodedContent([ new KeyValuePair("msisdn", "15555551212"), new KeyValuePair("to", "14445556543"), new KeyValuePair("message", "Your Lyft code is 12345"), - }); + ]); var response = await _httpClient.PostAsync($"{route}?token={token}", stringContent); @@ -151,7 +159,7 @@ public async Task CorrectlyFormattedButInvalidMessage() public async Task SendSMSMessageAsync() { var _client = await GetHttpClientWithValidBearerTokenAsync(); - var message = new SendMessageRequest { MediaURLs = Array.Empty(), Message = "This is an SMS Message test.", MSISDN = "2068589313", To = "2068589312" }; + var message = new SendMessageRequest { MediaURLs = [], Message = "This is an SMS Message test.", MSISDN = "2068589313", To = "2068589312" }; var response = await _client.PostAsJsonAsync("/message/send?test=true", message); var details = await response.Content.ReadFromJsonAsync(); Assert.NotNull(details); @@ -208,7 +216,7 @@ public async Task SendSMSMessageAsync() public async Task SendSMSGroupMessageAsync() { var _client = await GetHttpClientWithValidBearerTokenAsync(); - var message = new SendMessageRequest { MediaURLs = Array.Empty(), Message = "This is an SMS Group Message test.", MSISDN = "12068589313", To = "12068589312,12068589313,15036622288" }; + var message = new SendMessageRequest { MediaURLs = [], Message = "This is an SMS Group Message test.", MSISDN = "12068589313", To = "12068589312,12068589313,15036622288" }; var response = await _client.PostAsJsonAsync("/message/send?test=true", message); var details = await response.Content.ReadFromJsonAsync(); Assert.NotNull(details); @@ -220,15 +228,15 @@ public async Task SendSMSGroupMessageAsync() [Fact] public async Task MessageSendingTestAsync() { - var stringContent = new FormUrlEncodedContent(new[] - { + var stringContent = new FormUrlEncodedContent( + [ new KeyValuePair("msisdn", "15555551212"), new KeyValuePair("to", "14445556543"), new KeyValuePair("username", _configuration.GetConnectionString("PComNetUsername") ?? string.Empty), new KeyValuePair("password", _configuration.GetConnectionString("PComNetPassword") ?? string.Empty), new KeyValuePair("messagebody", "Your Lyft code is 12345"), - }); + ]); var response = await _httpClient.PostAsync("/message/send/test", stringContent); _output.WriteLine(await response.Content.ReadAsStringAsync()); Assert.True(response.IsSuccessStatusCode); @@ -257,12 +265,12 @@ public async Task BadToken() string route = "/api/inbound/1pcom"; string token = "thisIsNotAValidToken"; - var stringContent = new FormUrlEncodedContent(new[] - { + var stringContent = new FormUrlEncodedContent( + [ new KeyValuePair("msisdn", "15555551212"), new KeyValuePair("to", "14445556543"), new KeyValuePair("message", "Your Lyft code is 12345"), - }); + ]); var response = await _httpClient.PostAsync($"{route}?token={token}", stringContent); _output.WriteLine(await response.Content.ReadAsStringAsync()); diff --git a/Messaging/Migrations/20240418224229_RegistrationTesting.Designer.cs b/Messaging/Migrations/20240418224229_RegistrationTesting.Designer.cs new file mode 100644 index 00000000..8052c856 --- /dev/null +++ b/Messaging/Migrations/20240418224229_RegistrationTesting.Designer.cs @@ -0,0 +1,112 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Models; + +#nullable disable + +namespace Messaging.Migrations +{ + [DbContext(typeof(MessagingContext))] + [Migration("20240418224229_RegistrationTesting")] + partial class RegistrationTesting + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + modelBuilder.Entity("Models.ClientRegistration", b => + { + b.Property("ClientRegistrationId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AsDialed") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CallbackUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateLastTestMessageReceived") + .HasColumnType("TEXT"); + + b.Property("DateRegistered") + .HasColumnType("TEXT"); + + b.Property("RegisteredUpstream") + .HasColumnType("INTEGER"); + + b.Property("UpstreamStatusDescription") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ClientRegistrationId"); + + b.ToTable("ClientRegistrations"); + }); + + modelBuilder.Entity("Models.MessageRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateReceivedUTC") + .HasColumnType("TEXT"); + + b.Property("From") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MediaURLs") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MessageSource") + .HasColumnType("INTEGER"); + + b.Property("MessageType") + .HasColumnType("INTEGER"); + + b.Property("RawRequest") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RawResponse") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Succeeded") + .HasColumnType("INTEGER"); + + b.Property("To") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ToForward") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Messaging/Migrations/20240418224229_RegistrationTesting.cs b/Messaging/Migrations/20240418224229_RegistrationTesting.cs new file mode 100644 index 00000000..15008393 --- /dev/null +++ b/Messaging/Migrations/20240418224229_RegistrationTesting.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Messaging.Migrations +{ + /// + public partial class RegistrationTesting : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DateLastTestMessageReceived", + table: "ClientRegistrations", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DateLastTestMessageReceived", + table: "ClientRegistrations"); + } + } +} diff --git a/Messaging/Migrations/MessagingContextModelSnapshot.cs b/Messaging/Migrations/MessagingContextModelSnapshot.cs index f633d53c..184dc6fb 100644 --- a/Messaging/Migrations/MessagingContextModelSnapshot.cs +++ b/Messaging/Migrations/MessagingContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class MessagingContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); modelBuilder.Entity("Models.ClientRegistration", b => { @@ -35,6 +35,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("DateLastTestMessageReceived") + .HasColumnType("TEXT"); + b.Property("DateRegistered") .HasColumnType("TEXT"); diff --git a/Messaging/Program.cs b/Messaging/Program.cs index dfbc3336..02d61701 100644 --- a/Messaging/Program.cs +++ b/Messaging/Program.cs @@ -26,6 +26,7 @@ using Npgsql; +using NumberSearch.DataAccess.InvoiceNinja; using NumberSearch.DataAccess.Models; using Org.BouncyCastle.Ocsp; @@ -291,7 +292,13 @@ Description = "Boy I wish I had more to say about this, lmao." }); - app.MapGet("/client/test", Endpoints.TestClientAsync); + app.MapGet("/client/test", Endpoints.TestClientAsync) + .RequireAuthorization("api") + .WithOpenApi(x => new(x) + { + Summary = "Send a test message to a registered number to verify that it works correctly.", + Description = "Because this API is a middleman between the vendor and the client app we can send outbound SMS/MMS messages on behalf of a number that is registered with this app so that the vendor will reply to us with an inbound message matching the outbound message we sent. This allows us to verify that the registered number is routed and configured correctly for messaging service." + }); app.MapGet("/message/all", Endpoints.AllMessagesAsync) .RequireAuthorization("api") @@ -402,7 +409,7 @@ public static async Task, EmptyHttpResult, Probl // The signInManager already produced the needed response in the form of a cookie or bearer token. return TypedResults.Empty; } - + public static async Task, UnauthorizedHttpResult, SignInHttpResult, ChallengeHttpResult>> RefreshTokenAsync([FromBody] RefreshRequest refreshRequest, [FromServices] IServiceProvider sp) { @@ -425,7 +432,7 @@ await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not I var newPrincipal = await signInManager.CreateUserPrincipalAsync(user); return TypedResults.SignIn(newPrincipal, authenticationScheme: IdentityConstants.BearerScheme); } - + public static async Task, BadRequest, NotFound>> AllClientsAsync(bool? clear, MessagingContext db) { @@ -459,7 +466,7 @@ public static async Task, BadRequest, N } } - + public static async Task, BadRequest, NotFound>> UsageByClientAsync(MessagingContext db, string? asDialed) { @@ -579,7 +586,7 @@ public static async Task, BadRequest, NotFoun } } } - + public static async Task, BadRequest>> RegisterClientAsync(RegistrationRequest registration, AppSettings appSettings, MessagingContext db) { @@ -827,7 +834,7 @@ public static async Task, BadRequest, BadRequest>> RemoveClientRegistrationAsync(string asDialed, MessagingContext db) { @@ -860,9 +867,9 @@ public static async Task, BadRequest>> return TypedResults.Ok($"The registration for {asDialed} has been removed."); } - + public static async Task, NotFound, BadRequest>> - TestClientAsync(MessagingContext db, string? asDialed) + TestClientAsync(string? asDialed, AppSettings appSettings, MessagingContext db) { if (string.IsNullOrWhiteSpace(asDialed)) { @@ -879,11 +886,48 @@ public static async Task, NotFound, BadRequest x.AsDialed == asDialedNumber.DialedNumber).FirstOrDefaultAsync(); - if (existingRegistration is not null && existingRegistration.AsDialed == dialedNumber) + if (existingRegistration is not null && existingRegistration.AsDialed == asDialedNumber.DialedNumber) { // Send an outbound message from this number to this number, and suppress passing it foward to the client. + var request = new SendMessageRequest + { + MediaURLs = [], + MSISDN = existingRegistration.AsDialed, + To = existingRegistration.AsDialed, + Message = $"This is a test by Accelerate Networks to verify that {existingRegistration.AsDialed} is working correctly." + }; + + var dateTestSent = DateTime.Now; + + var result = await Endpoints.SendMessageAsync(request, false, appSettings, db); - return TypedResults.Ok($"Registration was found for {asDialed}"); + if (result.Result is Ok okResult && okResult.Value is not null) + { + // Wait for a while while the message round trips? We have 30 seconds before a time out so we'll check after 1+2+3+5+10 secounds before failing. + int[] delays = [1000, 2000, 3000, 5000, 10000]; + foreach (var delay in delays) + { + await Task.Delay(delay); + existingRegistration = await db.ClientRegistrations.Where(x => x.AsDialed == asDialedNumber.DialedNumber).FirstOrDefaultAsync(); + if (existingRegistration is not null && existingRegistration.DateLastTestMessageReceived >= dateTestSent) + { + return TypedResults.Ok($"Registration was found for {asDialed} and inbound and outbound SMS messaging is working correctly as of {existingRegistration.DateLastTestMessageReceived}."); + } + } + + return TypedResults.BadRequest($"The outbound test SMS was sent, but an inbound test SMS was not received for {asDialed}."); + } + else + { + if (result.Result is BadRequest badResult && badResult.Value is not null) + { + return TypedResults.BadRequest($"Outbound SMS could not be sent for {asDialed}. {JsonSerializer.Serialize(badResult.Value)}"); + } + else + { + return TypedResults.BadRequest($"Outbound SMS could not be sent for {asDialed}. {JsonSerializer.Serialize(result.Result)}"); + } + } } else { @@ -903,7 +947,7 @@ public static async Task, NotFound, BadRequest, NotFound, BadRequest>> AllMessagesAsync(MessagingContext db, string? asDialed) { @@ -969,7 +1013,7 @@ public static async Task, NotFound, BadReque } } } - + public static async Task, NotFound, BadRequest>> AllFailedMessagesAsync(MessagingContext db, DateTime start, DateTime end) { @@ -993,7 +1037,7 @@ public static async Task, NotFound, BadReque return TypedResults.BadRequest(ex.Message); } } - + public static async Task, NotFound, BadRequest>> ReplayMessageAsync(Guid id, AppSettings appSettings, MessagingContext db) { @@ -1080,7 +1124,7 @@ public static async Task, NotFound, BadRequest, BadRequest>> SendMessageAsync([Microsoft.AspNetCore.Mvc.FromBody] SendMessageRequest message, bool? test, AppSettings appSettings, MessagingContext db) { @@ -1153,15 +1197,16 @@ public static async Task, BadRequest numbers = new(); - foreach (var number in message.To.Split(',')) + string[] toParse = message.To.Split(','); + foreach (var number in toParse) { var checkTo = PhoneNumbersNA.PhoneNumber.TryParse(number, out var toPhoneNumber); if (checkTo && toPhoneNumber is not null) { var formattedNumber = toPhoneNumber.Type is PhoneNumbersNA.NumberType.ShortCode ? $"{toPhoneNumber.DialedNumber}" : $"1{toPhoneNumber.DialedNumber}"; - // Prevent the MSISDN from being included in the the recipients list. - if (formattedNumber != toForward.msisdn) + // Prevent the MSISDN from being included in the the recipients list. But allow circular messages where the MSISDN and To are the same to support testing. + if (formattedNumber != toForward.msisdn || toParse.Length is 1) { numbers.Add(formattedNumber); } @@ -1407,7 +1452,7 @@ public static async Task, BadRequest, BadRequest, Ok, UnauthorizedHttpResult>> InboundMMSFirstPointComAsync(HttpContext context, string token, AppSettings appSettings, WebApplication app, MessagingContext db) { @@ -1656,7 +1701,7 @@ public static async Task, BadRequest, Ok, BadRequest, Ok, UnauthorizedHttpResult>> InboundSMSFirstPointComAsync(HttpContext context, string token, AppSettings appSettings, MessagingContext db) { @@ -1800,12 +1845,24 @@ public static async Task, BadRequest, Ok, BadRequest, Ok, BadRequest, NotFound>> ClientByDialedNumberAsync(string asDialed, AppSettings appSettings, MessagingContext db) { @@ -1897,7 +1954,7 @@ public static async Task, BadRequest, Not return TypedResults.BadRequest(ex.Message); } } - + public static async Task, BadRequest>> SendTestAsync(HttpContext context, AppSettings appSettings) { @@ -1911,7 +1968,7 @@ public static async Task, BadRequest, BadRequest>> ForwardTestAsync(ForwardedMessage message, MessagingContext db) { @@ -2198,6 +2255,7 @@ public class ClientRegistration public string ClientSecret { get; set; } = string.Empty; public bool RegisteredUpstream { get; set; } = false; public string UpstreamStatusDescription { get; set; } = string.Empty; + public DateTime DateLastTestMessageReceived { get; set; } = DateTime.MinValue; //[DataType(DataType.EmailAddress)] //public string Email { get; set; } = string.Empty; //public bool EmailVerified { get; set; } = false; diff --git a/NumberSearch.DataAccess/NumberSearch.DataAccess.csproj b/NumberSearch.DataAccess/NumberSearch.DataAccess.csproj index 77205e45..186624c2 100644 --- a/NumberSearch.DataAccess/NumberSearch.DataAccess.csproj +++ b/NumberSearch.DataAccess/NumberSearch.DataAccess.csproj @@ -18,10 +18,10 @@ - + - + diff --git a/NumberSearch.Ops/NumberSearch.Ops.csproj b/NumberSearch.Ops/NumberSearch.Ops.csproj index 04ded81c..3df67231 100644 --- a/NumberSearch.Ops/NumberSearch.Ops.csproj +++ b/NumberSearch.Ops/NumberSearch.Ops.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - +