Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/NoteBookmark.Api.Tests/Endpoints/DeltaApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,45 @@ private async Task<Post> CreateAndSaveTestPost(string rowKey)
return post;
}

// ── Post ids ─────────────────────────────────────────────────────────────

[Fact]
public async Task GetPostIds_ReturnsIdsOfReadAndUnreadPosts()
{
// Arrange
var unreadPost = CreateTestPost("ids-unread-post-1");
var readPost = CreateTestPost("ids-read-post-1");
readPost.is_read = true;
await _client.PostAsJsonAsync("/api/posts/", unreadPost);
await _client.PostAsJsonAsync("/api/posts/", readPost);

// Act
var response = await _client.GetAsync("/api/posts/ids");

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var ids = await response.Content.ReadFromJsonAsync<List<string>>();
ids.Should().NotBeNull();
ids.Should().Contain(unreadPost.RowKey);
ids.Should().Contain(readPost.RowKey);
}

[Fact]
public async Task GetPostIds_DoesNotReturnDeletedPosts()
{
// Arrange
var post = CreateTestPost("ids-deleted-post-1");
await _client.PostAsJsonAsync("/api/posts/", post);
await _client.DeleteAsync($"/api/posts/{post.RowKey}");

// Act
var ids = await _client.GetFromJsonAsync<List<string>>("/api/posts/ids");

// Assert
ids.Should().NotBeNull();
ids.Should().NotContain(post.RowKey);
}

private static Post CreateTestPost(string rowKey)
{
return new Post
Expand Down
10 changes: 10 additions & 0 deletions src/NoteBookmark.Api/DataStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ orderby post.Timestamp
return lstPosts;
}

public List<string> GetPostIds()
{
var tblPosts = GetPostTable();
// Only project the key columns so the query stays small even with thousands of posts.
// Clients identify posts by Id, falling back to RowKey, so return the same value.
return tblPosts.Query<Post>(select: new[] { "RowKey", "id", "Id" })
.Select(p => p.Id ?? p.RowKey)
.ToList();
}

public Post? GetPost(string rowKey)
{
var tblPost = GetPostTable();
Expand Down
2 changes: 2 additions & 0 deletions src/NoteBookmark.Api/IDataStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace NoteBookmark.Api;
public interface IDataStorageService
{
public List<PostL> GetFilteredPosts(string filter);

public List<string> GetPostIds();

public Post? GetPost(string rowKey);

Expand Down
8 changes: 8 additions & 0 deletions src/NoteBookmark.Api/PostEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public static void MapPostEndpoints(this IEndpointRouteBuilder app)
.WithDescription("Get all unread posts");
endpoints.MapGet("/read", GetReadPosts)
.WithDescription("Get all read posts");
endpoints.MapGet("/ids", GetPostIds)
.WithDescription("Get the ids of all existing posts (read and unread)");
endpoints.MapGet("/{id}", Get)
.WithDescription("Get a post by id");
endpoints.MapGet("/{postId}/html", GetPostHtml)
Expand Down Expand Up @@ -57,6 +59,12 @@ static List<PostL> GetReadPosts(TableServiceClient tblClient, BlobServiceClient
return posts;
}

static List<string> GetPostIds(TableServiceClient tblClient, BlobServiceClient blobClient)
{
var dataStorageService = new DataStorageService(tblClient, blobClient);
return dataStorageService.GetPostIds();
}

static async Task<Results<ContentHttpResult, NotFound<ErrorMessage>>> GetPostHtml(
string postId, BlobServiceClient blobClient)
{
Expand Down
40 changes: 40 additions & 0 deletions src/NoteBookmark.MauiApp.Tests/LocalDataServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using NoteBookmark.Domain;
Expand Down Expand Up @@ -161,4 +162,43 @@ public async Task SaveSettings_ShouldStoreAndRetrieveFontSize()
retrieved.Should().NotBeNull();
retrieved!.FontSize.Should().Be("large");
}

[Fact]
public async Task RemovePostsAsync_ShouldPermanentlyRemoveOnlyGivenPosts()
{
await _sut.SavePostsAsync(new[]
{
new Post { Id = "p1", PartitionKey = "pk", RowKey = "p1", Title = "One", DateModified = DateTime.UtcNow },
new Post { Id = "p2", PartitionKey = "pk", RowKey = "p2", Title = "Two", DateModified = DateTime.UtcNow },
new Post { Id = "p3", PartitionKey = "pk", RowKey = "p3", Title = "Three", DateModified = DateTime.UtcNow }
});
await _sut.SavePostAsync(new Post { Id = "p2", PartitionKey = "pk", RowKey = "p2", Title = "Two edited" }, isPendingSync: true);

await _sut.RemovePostsAsync(new[] { "p1", "p2", "missing" });

var remaining = await _sut.GetPostsAsync();
remaining.Select(p => p.Id).Should().BeEquivalentTo(new[] { "p3" });
(await _sut.GetPendingSyncPostsAsync()).Should().BeEmpty();
}

[Fact]
public async Task RemovePostsAsync_WithNoIds_ShouldDoNothing()
{
await _sut.SavePostAsync(new Post { Id = "p1", PartitionKey = "pk", RowKey = "p1", Title = "One" });

await _sut.RemovePostsAsync(Array.Empty<string>());

(await _sut.GetPostsAsync()).Should().HaveCount(1);
}

[Fact]
public async Task SavePostsAsync_ShouldPreservePendingSyncFlag_ForLocallyEditedPosts()
{
await _sut.SavePostAsync(new Post { Id = "p1", PartitionKey = "pk", RowKey = "p1", Title = "Local edit" }, isPendingSync: true);

await _sut.SavePostsAsync(new[] { new Post { Id = "p1", PartitionKey = "pk", RowKey = "p1", Title = "From server" } });

var pending = await _sut.GetPendingSyncPostsAsync();
pending.Should().ContainSingle(p => p.Id == "p1");
}
}
Loading
Loading