Skip to content

Commit 39f80e4

Browse files
author
davidwei
committed
Prepend user-owned note to search results when query is an integer on first page
1 parent f569765 commit 39f80e4

File tree

2 files changed

+211
-0
lines changed

2 files changed

+211
-0
lines changed

src/HappyNotes.Services/SearchService.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,25 @@ public SearchService(IDatabaseClient client, HttpClient httpClient, ManticoreCon
6666
}
6767
}
6868

69+
// Enhancement: if query is an integer and this is the first page,
70+
// check if the note ID belongs to the current user and prepend it to results
71+
if (pageNumber == 1 && long.TryParse(query, out long noteId))
72+
{
73+
// Check if this note ID belongs to the current user
74+
var notes = await _client.SqlQueryAsync<Note>(
75+
"SELECT Id, UserId FROM Note WHERE Id = @noteId",
76+
new { noteId });
77+
var note = notes.FirstOrDefault();
78+
79+
if (note != null && note.UserId == userId)
80+
{
81+
// Remove the note from the list if it's already there to avoid duplication
82+
noteIdList.RemoveAll(id => id == noteId);
83+
// Prepend it to the beginning
84+
noteIdList.Insert(0, noteId);
85+
}
86+
}
87+
6988
return (noteIdList, (int)total);
7089
}
7190

tests/HappyNotes.Services.Tests/SearchServiceTests.cs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,196 @@ public async Task PurgeDeletedNotesFromIndexAsync_DeletedNotes_RemovesThem()
296296
req.RequestUri.ToString().Contains("json/delete")),
297297
ItExpr.IsAny<CancellationToken>());
298298
}
299+
300+
[Test]
301+
public async Task GetNoteIdsByKeywordAsync_IntegerQuery_FirstPage_UserOwnsNote_PrependsToResults()
302+
{
303+
// Arrange
304+
long userId = 1;
305+
string query = "123";
306+
int pageNumber = 1;
307+
int pageSize = 10;
308+
309+
// Mock regular search results
310+
_mockHandler.Protected()
311+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
312+
.ReturnsAsync(new HttpResponseMessage
313+
{
314+
StatusCode = HttpStatusCode.OK,
315+
Content = new StringContent("{\"took\":0,\"timed_out\":false,\"hits\":{\"total\":2,\"hits\":[{\"_id\":456,\"_source\":{\"id\":456}},{\"_id\":789,\"_source\":{\"id\":789}}]}}")
316+
});
317+
318+
// Mock database query to return user's note
319+
_mockDatabaseClient.Setup(x => x.SqlQueryAsync<Note>(
320+
It.IsAny<string>(),
321+
It.IsAny<object>()))
322+
.ReturnsAsync(new List<Note> { new Note { Id = 123, UserId = 1 } });
323+
324+
// Act
325+
var result = await _searchService.GetNoteIdsByKeywordAsync(userId, query, pageNumber, pageSize);
326+
327+
// Assert
328+
Assert.IsNotNull(result);
329+
Assert.That(result.Item1[0], Is.EqualTo(123)); // Note 123 should be first
330+
Assert.That(result.Item1.Count, Is.EqualTo(3)); // Should have 3 notes total
331+
CollectionAssert.AreEqual(new List<long> { 123, 456, 789 }, result.Item1);
332+
}
333+
334+
[Test]
335+
public async Task GetNoteIdsByKeywordAsync_IntegerQuery_FirstPage_UserDoesNotOwnNote_DoesNotPrepend()
336+
{
337+
// Arrange
338+
long userId = 1;
339+
string query = "123";
340+
int pageNumber = 1;
341+
int pageSize = 10;
342+
343+
// Mock regular search results
344+
_mockHandler.Protected()
345+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
346+
.ReturnsAsync(new HttpResponseMessage
347+
{
348+
StatusCode = HttpStatusCode.OK,
349+
Content = new StringContent("{\"took\":0,\"timed_out\":false,\"hits\":{\"total\":2,\"hits\":[{\"_id\":456,\"_source\":{\"id\":456}},{\"_id\":789,\"_source\":{\"id\":789}}]}}")
350+
});
351+
352+
// Mock database query to return note owned by different user
353+
_mockDatabaseClient.Setup(x => x.SqlQueryAsync<Note>(
354+
It.IsAny<string>(),
355+
It.IsAny<object>()))
356+
.ReturnsAsync(new List<Note> { new Note { Id = 123, UserId = 2 } }); // Different user
357+
358+
// Act
359+
var result = await _searchService.GetNoteIdsByKeywordAsync(userId, query, pageNumber, pageSize);
360+
361+
// Assert
362+
Assert.IsNotNull(result);
363+
Assert.That(result.Item1.Count, Is.EqualTo(2)); // Should remain 2 notes
364+
CollectionAssert.AreEqual(new List<long> { 456, 789 }, result.Item1);
365+
}
366+
367+
[Test]
368+
public async Task GetNoteIdsByKeywordAsync_IntegerQuery_SecondPage_DoesNotPrepend()
369+
{
370+
// Arrange
371+
long userId = 1;
372+
string query = "123";
373+
int pageNumber = 2; // Not first page
374+
int pageSize = 10;
375+
376+
// Mock regular search results
377+
_mockHandler.Protected()
378+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
379+
.ReturnsAsync(new HttpResponseMessage
380+
{
381+
StatusCode = HttpStatusCode.OK,
382+
Content = new StringContent("{\"took\":0,\"timed_out\":false,\"hits\":{\"total\":2,\"hits\":[{\"_id\":456,\"_source\":{\"id\":456}},{\"_id\":789,\"_source\":{\"id\":789}}]}}")
383+
});
384+
385+
// Act
386+
var result = await _searchService.GetNoteIdsByKeywordAsync(userId, query, pageNumber, pageSize);
387+
388+
// Assert
389+
Assert.IsNotNull(result);
390+
Assert.That(result.Item1.Count, Is.EqualTo(2)); // Should remain 2 notes
391+
CollectionAssert.AreEqual(new List<long> { 456, 789 }, result.Item1);
392+
// Verify database was not called since it's not first page
393+
_mockDatabaseClient.Verify(x => x.SqlQueryAsync<Note>(It.IsAny<string>(), It.IsAny<object>()), Times.Never);
394+
}
395+
396+
[Test]
397+
public async Task GetNoteIdsByKeywordAsync_IntegerQuery_NoteNotFound_DoesNotPrepend()
398+
{
399+
// Arrange
400+
long userId = 1;
401+
string query = "123";
402+
int pageNumber = 1;
403+
int pageSize = 10;
404+
405+
// Mock regular search results
406+
_mockHandler.Protected()
407+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
408+
.ReturnsAsync(new HttpResponseMessage
409+
{
410+
StatusCode = HttpStatusCode.OK,
411+
Content = new StringContent("{\"took\":0,\"timed_out\":false,\"hits\":{\"total\":2,\"hits\":[{\"_id\":456,\"_source\":{\"id\":456}},{\"_id\":789,\"_source\":{\"id\":789}}]}}")
412+
});
413+
414+
// Mock database query to return empty list (note not found)
415+
_mockDatabaseClient.Setup(x => x.SqlQueryAsync<Note>(
416+
It.IsAny<string>(),
417+
It.IsAny<object>()))
418+
.ReturnsAsync(new List<Note>());
419+
420+
// Act
421+
var result = await _searchService.GetNoteIdsByKeywordAsync(userId, query, pageNumber, pageSize);
422+
423+
// Assert
424+
Assert.IsNotNull(result);
425+
Assert.That(result.Item1.Count, Is.EqualTo(2)); // Should remain 2 notes
426+
CollectionAssert.AreEqual(new List<long> { 456, 789 }, result.Item1);
427+
}
428+
429+
[Test]
430+
public async Task GetNoteIdsByKeywordAsync_IntegerQuery_FirstPage_NoteAlreadyInResults_DoesNotDuplicate()
431+
{
432+
// Arrange
433+
long userId = 1;
434+
string query = "123";
435+
int pageNumber = 1;
436+
int pageSize = 10;
437+
438+
// Mock regular search results including the note ID we're searching for
439+
_mockHandler.Protected()
440+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
441+
.ReturnsAsync(new HttpResponseMessage
442+
{
443+
StatusCode = HttpStatusCode.OK,
444+
Content = new StringContent("{\"took\":0,\"timed_out\":false,\"hits\":{\"total\":3,\"hits\":[{\"_id\":456,\"_source\":{\"id\":456}},{\"_id\":123,\"_source\":{\"id\":123}},{\"_id\":789,\"_source\":{\"id\":789}}]}}")
445+
});
446+
447+
// Mock database query to return user's note
448+
_mockDatabaseClient.Setup(x => x.SqlQueryAsync<Note>(
449+
It.IsAny<string>(),
450+
It.IsAny<object>()))
451+
.ReturnsAsync(new List<Note> { new Note { Id = 123, UserId = 1 } });
452+
453+
// Act
454+
var result = await _searchService.GetNoteIdsByKeywordAsync(userId, query, pageNumber, pageSize);
455+
456+
// Assert
457+
Assert.IsNotNull(result);
458+
Assert.That(result.Item1[0], Is.EqualTo(123)); // Note 123 should be first
459+
Assert.That(result.Item1.Count, Is.EqualTo(3)); // Should have 3 notes total (no duplication)
460+
CollectionAssert.AreEqual(new List<long> { 123, 456, 789 }, result.Item1);
461+
}
462+
463+
[Test]
464+
public async Task GetNoteIdsByKeywordAsync_NonIntegerQuery_DoesNotPrepend()
465+
{
466+
// Arrange
467+
long userId = 1;
468+
string query = "test search";
469+
int pageNumber = 1;
470+
int pageSize = 10;
471+
472+
// Mock regular search results
473+
_mockHandler.Protected()
474+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
475+
.ReturnsAsync(new HttpResponseMessage
476+
{
477+
StatusCode = HttpStatusCode.OK,
478+
Content = new StringContent("{\"took\":0,\"timed_out\":false,\"hits\":{\"total\":2,\"hits\":[{\"_id\":456,\"_source\":{\"id\":456}},{\"_id\":789,\"_source\":{\"id\":789}}]}}")
479+
});
480+
481+
// Act
482+
var result = await _searchService.GetNoteIdsByKeywordAsync(userId, query, pageNumber, pageSize);
483+
484+
// Assert
485+
Assert.IsNotNull(result);
486+
Assert.That(result.Item1.Count, Is.EqualTo(2)); // Should remain 2 notes
487+
CollectionAssert.AreEqual(new List<long> { 456, 789 }, result.Item1);
488+
// Verify database was not called since query is not an integer
489+
_mockDatabaseClient.Verify(x => x.SqlQueryAsync<Note>(It.IsAny<string>(), It.IsAny<object>()), Times.Never);
490+
}
299491
}

0 commit comments

Comments
 (0)