diff --git a/Directory.Build.props b/Directory.Build.props index c4bff0d..ee49ac9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.3.0 + 1.3.1 net10.0 enable enable diff --git a/src/NoteBookmark.BlazorApp.Tests/Tests/PostNoteClientTests.cs b/src/NoteBookmark.BlazorApp.Tests/Tests/PostNoteClientTests.cs new file mode 100644 index 0000000..0f5f715 --- /dev/null +++ b/src/NoteBookmark.BlazorApp.Tests/Tests/PostNoteClientTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NoteBookmark.Domain; +using NoteBookmark.SharedUI; +using Xunit; + +namespace NoteBookmark.BlazorApp.Tests.Tests; + +public class PostNoteClientTests +{ + private class TestHttpMessageHandler : HttpMessageHandler + { + public Func Handler { get; set; } = null!; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(Handler(request)); + } + } + + [Fact] + public async Task CreateReadingNotes_WithNullTags_ShouldNotThrowAndShouldFallbackToMiscellaneous() + { + // Arrange + var handler = new TestHttpMessageHandler(); + using var client = new HttpClient(handler) { BaseAddress = new Uri("http://localhost/") }; + var postNoteClient = new PostNoteClient(client); + + var notesList = new List + { + new ReadingNote { Title = "Note 1", Tags = null, Category = null } + }; + + handler.Handler = (req) => + { + if (req.RequestUri!.PathAndQuery.Contains("GetNextReadingNotesCounter")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("708") + }; + } + if (req.RequestUri!.PathAndQuery.Contains("GetNotesForSummary/708")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(notesList), System.Text.Encoding.UTF8, "application/json") + }; + } + return new HttpResponseMessage(HttpStatusCode.NotFound); + }; + + // Act + var result = await postNoteClient.CreateReadingNotes(); + + // Assert + result.Should().NotBeNull(); + result.Notes.Should().ContainKey("Miscellaneous"); + result.Notes["Miscellaneous"].Should().HaveCount(1); + result.Notes["Miscellaneous"][0].Title.Should().Be("Note 1"); + } + + [Fact] + public async Task CreateReadingNotes_WithEmptyTags_ShouldNotThrowAndShouldFallbackToMiscellaneous() + { + // Arrange + var handler = new TestHttpMessageHandler(); + using var client = new HttpClient(handler) { BaseAddress = new Uri("http://localhost/") }; + var postNoteClient = new PostNoteClient(client); + + var notesList = new List + { + new ReadingNote { Title = "Note 2", Tags = "", Category = null } + }; + + handler.Handler = (req) => + { + if (req.RequestUri!.PathAndQuery.Contains("GetNextReadingNotesCounter")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("708") + }; + } + if (req.RequestUri!.PathAndQuery.Contains("GetNotesForSummary/708")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(notesList), System.Text.Encoding.UTF8, "application/json") + }; + } + return new HttpResponseMessage(HttpStatusCode.NotFound); + }; + + // Act + var result = await postNoteClient.CreateReadingNotes(); + + // Assert + result.Should().NotBeNull(); + result.Notes.Should().ContainKey("Miscellaneous"); + result.Notes["Miscellaneous"].Should().HaveCount(1); + result.Notes["Miscellaneous"][0].Title.Should().Be("Note 2"); + } + + [Fact] + public async Task CreateReadingNotes_WithValidTags_ShouldGroupByCategory() + { + // Arrange + var handler = new TestHttpMessageHandler(); + using var client = new HttpClient(handler) { BaseAddress = new Uri("http://localhost/") }; + var postNoteClient = new PostNoteClient(client); + + var notesList = new List + { + new ReadingNote { Title = "Note 3", Tags = "cloud,dev", Category = null } + }; + + handler.Handler = (req) => + { + if (req.RequestUri!.PathAndQuery.Contains("GetNextReadingNotesCounter")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("708") + }; + } + if (req.RequestUri!.PathAndQuery.Contains("GetNotesForSummary/708")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(notesList), System.Text.Encoding.UTF8, "application/json") + }; + } + return new HttpResponseMessage(HttpStatusCode.NotFound); + }; + + // Act + var result = await postNoteClient.CreateReadingNotes(); + + // Assert + result.Should().NotBeNull(); + result.Notes.Should().ContainKey("Cloud"); + result.Notes["Cloud"].Should().HaveCount(1); + result.Notes["Cloud"][0].Title.Should().Be("Note 3"); + } +} diff --git a/src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj b/src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj index 2243907..6e5a5be 100644 --- a/src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj +++ b/src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj @@ -44,9 +44,9 @@ c5m.notebookmark.mauiapp - 1.3.0 - 3 - 1.3.0 + 1.3.1 + 4 + 1.3.1 None diff --git a/src/NoteBookmark.SharedUI/PostNoteClient.cs b/src/NoteBookmark.SharedUI/PostNoteClient.cs index c1721c6..7d0e215 100644 --- a/src/NoteBookmark.SharedUI/PostNoteClient.cs +++ b/src/NoteBookmark.SharedUI/PostNoteClient.cs @@ -1,214 +1,214 @@ -using System; -using System.Net.Http.Json; -using NoteBookmark.Domain; - -namespace NoteBookmark.SharedUI; - -public class PostNoteClient(HttpClient httpClient) : IDataService -{ - public async Task> GetUnreadPosts() - { - var posts = await httpClient.GetFromJsonAsync>("api/posts"); - return posts ?? new List(); - } - - public async Task> GetReadPosts() - { - var posts = await httpClient.GetFromJsonAsync>("api/posts/read"); - return posts ?? new List(); - } - - public async Task> GetSummaries() - { - var summaries = await httpClient.GetFromJsonAsync>("api/summary"); - return summaries ?? new List(); - } - - public async Task CreateNote(Note note) - { - var rnCounter = await httpClient.GetStringAsync("api/settings/GetNextReadingNotesCounter"); - note.PartitionKey = rnCounter; - var response = await httpClient.PostAsJsonAsync("api/notes/note", note); - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(); - throw new HttpRequestException($"Server returned Bad Request: {errorContent}", null, response.StatusCode); - } - } - - public async Task GetNote(string noteId) - { - var note = await httpClient.GetFromJsonAsync($"api/notes/note/{noteId}"); - return note; - } - - public async Task UpdateNote(Note note) - { - var response = await httpClient.PutAsJsonAsync("api/notes/note", note); - return response.IsSuccessStatusCode; - } - - public async Task DeleteNote(string noteId) - { - var response = await httpClient.DeleteAsync($"api/notes/note/{noteId}"); - return response.IsSuccessStatusCode; - } - - public async Task CreateReadingNotes() - { - var rnCounter = await httpClient.GetStringAsync("api/settings/GetNextReadingNotesCounter"); - var readingNotes = new ReadingNotes(rnCounter); - - var unsortedNotes = await httpClient.GetFromJsonAsync>($"api/notes/GetNotesForSummary/{rnCounter}"); - - if (unsortedNotes == null || unsortedNotes.Count == 0) - { - return readingNotes; - } - - Dictionary> sortedNotes = GroupNotesByCategory(unsortedNotes); - - readingNotes.Notes = sortedNotes; - readingNotes.Tags = readingNotes.GetAllUniqueTags(); - - return readingNotes; - } - - public async Task GetReadingNotes(string number) - { - ReadingNotes? readingNotes; - readingNotes = await httpClient.GetFromJsonAsync($"api/summary/{number}"); - return readingNotes; - } - - private Dictionary> GroupNotesByCategory(List notes) - { - var sortedNotes = new Dictionary>(); - - foreach (var note in notes) - { - var tags = note.Tags?.ToLower().Split(',') ?? Array.Empty(); - - if (string.IsNullOrEmpty(note.Category)) - { - note.Category = NoteCategories.GetCategory(tags[0]); - } - - string category = note.Category; - if (sortedNotes.ContainsKey(category)) - { - sortedNotes[category].Add(note); - } - else - { - sortedNotes.Add(category, new List { note }); - } - } - - return sortedNotes; - } - - public async Task SaveReadingNotes(ReadingNotes readingNotes) - { - var response = await httpClient.PostAsJsonAsync("api/notes/SaveReadingNotes", readingNotes); - - string jsonURL = ((string)await response.Content.ReadAsStringAsync()).Replace("\"", ""); - - if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(jsonURL)) - { - var summary = new Summary - { - PartitionKey = readingNotes.Number, - RowKey = readingNotes.Number, - Title = readingNotes.Title, - Id = readingNotes.Number, - IsGenerated = "true", - PublishedURL = readingNotes.PublishedUrl, - FileName = jsonURL - }; - - var summaryResponse = await httpClient.PostAsJsonAsync("api/summary/summary", summary); - return summaryResponse.IsSuccessStatusCode; - } - - return false; - } - - public async Task GetPost(string id) - { - var post = await httpClient.GetFromJsonAsync($"api/posts/{id}"); - return post; - } - - public async Task SavePost(Post post) - { - var response = await httpClient.PostAsJsonAsync("api/posts", post); - return response.IsSuccessStatusCode; - } - - public async Task GetSettings() - { - var settings = await httpClient.GetFromJsonAsync("api/settings"); - return settings; - } - - public async Task SaveSettings(Settings settings) - { - var response = await httpClient.PostAsJsonAsync("api/settings/SaveSettings", settings); - return response.IsSuccessStatusCode; - } - - public async Task ExtractPostDetailsAndSave(string url) - { - var requestBody = new { url = url }; - var response = await httpClient.PostAsJsonAsync($"api/posts/extractPostDetails", requestBody); - return response.IsSuccessStatusCode; - } - - public async Task DeletePost(string id) - { - var response = await httpClient.DeleteAsync($"api/posts/{id}"); - return response.IsSuccessStatusCode; - } - - public async Task SaveReadingNotesMarkdown(string markdown, string number) - { - var request = new { Markdown = markdown }; - var response = await httpClient.PostAsJsonAsync($"api/summary/{number}/markdown", request); - return response.IsSuccessStatusCode; - } - - public async Task GetPostHtmlAsync(string postId) - { - var response = await httpClient.GetAsync($"api/posts/{postId}/html"); - if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return null; - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } - - public async Task> GetPostsModifiedAfter(DateTime modifiedAfter) - { - var encoded = System.Web.HttpUtility.UrlEncode(modifiedAfter.ToUniversalTime().ToString("O")); - var unread = await httpClient.GetFromJsonAsync>($"api/posts?modifiedAfter={encoded}") ?? new List(); - var read = await httpClient.GetFromJsonAsync>($"api/posts/read?modifiedAfter={encoded}") ?? new List(); - return unread.Concat(read).ToList(); - } - - public async Task> GetNotesModifiedAfter(DateTime modifiedAfter) - { - try - { - var encoded = System.Web.HttpUtility.UrlEncode(modifiedAfter.ToUniversalTime().ToString("O")); - var notes = await httpClient.GetFromJsonAsync>($"api/notes?modifiedAfter={encoded}"); - return notes ?? new List(); - } - catch (System.Net.Http.HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) - { - return new List(); - } - } - - public Task SyncAsync() => Task.CompletedTask; - public bool IsOffline => false; - public bool CanSync => false; -} +using System; +using System.Net.Http.Json; +using NoteBookmark.Domain; + +namespace NoteBookmark.SharedUI; + +public class PostNoteClient(HttpClient httpClient) : IDataService +{ + public async Task> GetUnreadPosts() + { + var posts = await httpClient.GetFromJsonAsync>("api/posts"); + return posts ?? new List(); + } + + public async Task> GetReadPosts() + { + var posts = await httpClient.GetFromJsonAsync>("api/posts/read"); + return posts ?? new List(); + } + + public async Task> GetSummaries() + { + var summaries = await httpClient.GetFromJsonAsync>("api/summary"); + return summaries ?? new List(); + } + + public async Task CreateNote(Note note) + { + var rnCounter = await httpClient.GetStringAsync("api/settings/GetNextReadingNotesCounter"); + note.PartitionKey = rnCounter; + var response = await httpClient.PostAsJsonAsync("api/notes/note", note); + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"Server returned Bad Request: {errorContent}", null, response.StatusCode); + } + } + + public async Task GetNote(string noteId) + { + var note = await httpClient.GetFromJsonAsync($"api/notes/note/{noteId}"); + return note; + } + + public async Task UpdateNote(Note note) + { + var response = await httpClient.PutAsJsonAsync("api/notes/note", note); + return response.IsSuccessStatusCode; + } + + public async Task DeleteNote(string noteId) + { + var response = await httpClient.DeleteAsync($"api/notes/note/{noteId}"); + return response.IsSuccessStatusCode; + } + + public async Task CreateReadingNotes() + { + var rnCounter = await httpClient.GetStringAsync("api/settings/GetNextReadingNotesCounter"); + var readingNotes = new ReadingNotes(rnCounter); + + var unsortedNotes = await httpClient.GetFromJsonAsync>($"api/notes/GetNotesForSummary/{rnCounter}"); + + if (unsortedNotes == null || unsortedNotes.Count == 0) + { + return readingNotes; + } + + Dictionary> sortedNotes = GroupNotesByCategory(unsortedNotes); + + readingNotes.Notes = sortedNotes; + readingNotes.Tags = readingNotes.GetAllUniqueTags(); + + return readingNotes; + } + + public async Task GetReadingNotes(string number) + { + ReadingNotes? readingNotes; + readingNotes = await httpClient.GetFromJsonAsync($"api/summary/{number}"); + return readingNotes; + } + + private Dictionary> GroupNotesByCategory(List notes) + { + var sortedNotes = new Dictionary>(); + + foreach (var note in notes) + { + var tags = note.Tags?.ToLower().Split(',') ?? Array.Empty(); + + if (string.IsNullOrEmpty(note.Category)) + { + note.Category = NoteCategories.GetCategory(tags.Length > 0 ? tags[0] : null); + } + + string category = note.Category; + if (sortedNotes.ContainsKey(category)) + { + sortedNotes[category].Add(note); + } + else + { + sortedNotes.Add(category, new List { note }); + } + } + + return sortedNotes; + } + + public async Task SaveReadingNotes(ReadingNotes readingNotes) + { + var response = await httpClient.PostAsJsonAsync("api/notes/SaveReadingNotes", readingNotes); + + string jsonURL = ((string)await response.Content.ReadAsStringAsync()).Replace("\"", ""); + + if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(jsonURL)) + { + var summary = new Summary + { + PartitionKey = readingNotes.Number, + RowKey = readingNotes.Number, + Title = readingNotes.Title, + Id = readingNotes.Number, + IsGenerated = "true", + PublishedURL = readingNotes.PublishedUrl, + FileName = jsonURL + }; + + var summaryResponse = await httpClient.PostAsJsonAsync("api/summary/summary", summary); + return summaryResponse.IsSuccessStatusCode; + } + + return false; + } + + public async Task GetPost(string id) + { + var post = await httpClient.GetFromJsonAsync($"api/posts/{id}"); + return post; + } + + public async Task SavePost(Post post) + { + var response = await httpClient.PostAsJsonAsync("api/posts", post); + return response.IsSuccessStatusCode; + } + + public async Task GetSettings() + { + var settings = await httpClient.GetFromJsonAsync("api/settings"); + return settings; + } + + public async Task SaveSettings(Settings settings) + { + var response = await httpClient.PostAsJsonAsync("api/settings/SaveSettings", settings); + return response.IsSuccessStatusCode; + } + + public async Task ExtractPostDetailsAndSave(string url) + { + var requestBody = new { url = url }; + var response = await httpClient.PostAsJsonAsync($"api/posts/extractPostDetails", requestBody); + return response.IsSuccessStatusCode; + } + + public async Task DeletePost(string id) + { + var response = await httpClient.DeleteAsync($"api/posts/{id}"); + return response.IsSuccessStatusCode; + } + + public async Task SaveReadingNotesMarkdown(string markdown, string number) + { + var request = new { Markdown = markdown }; + var response = await httpClient.PostAsJsonAsync($"api/summary/{number}/markdown", request); + return response.IsSuccessStatusCode; + } + + public async Task GetPostHtmlAsync(string postId) + { + var response = await httpClient.GetAsync($"api/posts/{postId}/html"); + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return null; + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + public async Task> GetPostsModifiedAfter(DateTime modifiedAfter) + { + var encoded = System.Web.HttpUtility.UrlEncode(modifiedAfter.ToUniversalTime().ToString("O")); + var unread = await httpClient.GetFromJsonAsync>($"api/posts?modifiedAfter={encoded}") ?? new List(); + var read = await httpClient.GetFromJsonAsync>($"api/posts/read?modifiedAfter={encoded}") ?? new List(); + return unread.Concat(read).ToList(); + } + + public async Task> GetNotesModifiedAfter(DateTime modifiedAfter) + { + try + { + var encoded = System.Web.HttpUtility.UrlEncode(modifiedAfter.ToUniversalTime().ToString("O")); + var notes = await httpClient.GetFromJsonAsync>($"api/notes?modifiedAfter={encoded}"); + return notes ?? new List(); + } + catch (System.Net.Http.HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return new List(); + } + } + + public Task SyncAsync() => Task.CompletedTask; + public bool IsOffline => false; + public bool CanSync => false; +}