diff --git a/src/TextServices.Search.Api/Features/Autocomplete/AutocompleteEndpoints.cs b/src/TextServices.Search.Api/Features/Autocomplete/AutocompleteEndpoints.cs index 241c3dd..00c1004 100644 --- a/src/TextServices.Search.Api/Features/Autocomplete/AutocompleteEndpoints.cs +++ b/src/TextServices.Search.Api/Features/Autocomplete/AutocompleteEndpoints.cs @@ -14,8 +14,10 @@ internal static IEndpointRouteBuilder MapAutocompleteEndpoints(this IEndpointRou IOptions options, HttpContext ctx) => { + if (string.IsNullOrWhiteSpace(q)) return Results.BadRequest(); + var resolved = EndpointHelpers.Resolve(options.Value, ctx, "autocomplete/v1/", id, q); - var result = await sender.Send(new AutocompleteRequest(id, q ?? string.Empty, resolved.SelfUrl)); + var result = await sender.Send(new AutocompleteRequest(id, q, resolved.SelfUrl)); if (result == null) return Results.NotFound(); return Results.Json(result, contentType: "application/ld+json"); }); @@ -26,8 +28,10 @@ internal static IEndpointRouteBuilder MapAutocompleteEndpoints(this IEndpointRou IOptions options, HttpContext ctx) => { + if (string.IsNullOrWhiteSpace(q)) return Results.BadRequest(); + var resolved = EndpointHelpers.Resolve(options.Value, ctx, "autocomplete/v2/", id, q); - var result = await sender.Send(new AutocompleteV2Request(id, q ?? string.Empty, resolved.SelfUrl)); + var result = await sender.Send(new AutocompleteV2Request(id, q, resolved.SelfUrl)); if (result == null) return Results.NotFound(); return Results.Json(result, contentType: "application/ld+json"); }); diff --git a/src/TextServices.Search.Api/Features/Search/SearchEndpoints.cs b/src/TextServices.Search.Api/Features/Search/SearchEndpoints.cs index 78045ef..9c9e6dd 100644 --- a/src/TextServices.Search.Api/Features/Search/SearchEndpoints.cs +++ b/src/TextServices.Search.Api/Features/Search/SearchEndpoints.cs @@ -14,8 +14,10 @@ internal static IEndpointRouteBuilder MapSearchEndpoints(this IEndpointRouteBuil IOptions options, HttpContext ctx) => { + if (string.IsNullOrWhiteSpace(q)) return Results.BadRequest(); + var resolved = EndpointHelpers.Resolve(options.Value, ctx, "search/v1/", id, q); - var result = await sender.Send(new SearchRequest(id, q ?? string.Empty, resolved.SelfUrl, resolved.ResourceUrl)); + var result = await sender.Send(new SearchRequest(id, q, resolved.SelfUrl, resolved.ResourceUrl)); if (result == null) return Results.NotFound(); result.Ignored = EndpointHelpers.GetIgnoredParams(ctx); return Results.Json(result, contentType: "application/ld+json"); @@ -27,8 +29,10 @@ internal static IEndpointRouteBuilder MapSearchEndpoints(this IEndpointRouteBuil IOptions options, HttpContext ctx) => { + if (string.IsNullOrWhiteSpace(q)) return Results.BadRequest(); + var resolved = EndpointHelpers.Resolve(options.Value, ctx, "search/v2/", id, q); - var result = await sender.Send(new SearchV2Request(id, q ?? string.Empty, resolved.SelfUrl, resolved.ResourceUrl)); + var result = await sender.Send(new SearchV2Request(id, q, resolved.SelfUrl, resolved.ResourceUrl)); if (result == null) return Results.NotFound(); result.Ignored = EndpointHelpers.GetIgnoredParams(ctx); return Results.Json(result, contentType: "application/ld+json"); diff --git a/src/TextServices.Tests.E2E/BuildAndSearchTests.cs b/src/TextServices.Tests.E2E/BuildAndSearchTests.cs index b8fcc4a..a75a436 100644 --- a/src/TextServices.Tests.E2E/BuildAndSearchTests.cs +++ b/src/TextServices.Tests.E2E/BuildAndSearchTests.cs @@ -169,15 +169,11 @@ public async Task Search_KnownWord_ReturnsHits() } [Fact] - public async Task Search_EmptyQuery_ReturnsEmptyNotError() + public async Task Search_EmptyQuery_Returns400() { - var id = await BuildFixtureAsync("e2e/search-empty"); - - var response = await ctx.SearchClient.GetAsync($"/search/v1/{id}?q="); - response.StatusCode.ShouldBe(HttpStatusCode.OK); - - var body = JsonNode.Parse(await response.Content.ReadAsStringAsync())!; - body["resources"]!.AsArray().Count.ShouldBe(0); + // Query validation happens before the job is looked up, so no fixture is needed. + var response = await ctx.SearchClient.GetAsync("/search/v1/no/such/id?q="); + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); } [Fact] @@ -187,6 +183,15 @@ public async Task Search_UnknownId_Returns404() response.StatusCode.ShouldBe(HttpStatusCode.NotFound); } + [Fact] + public async Task Search_MissingQuery_Returns400EvenForUnknownId() + { + // Query validation happens before the job is looked up — an unknown id + // with a missing query returns 400, not 404. + var response = await ctx.SearchClient.GetAsync("/search/v2/no/such/id"); + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + } + // ------------------------------------------------------------------------- // Autocomplete API // ------------------------------------------------------------------------- @@ -204,6 +209,14 @@ public async Task Autocomplete_ShortQuery_ReturnsEmptyTermList() body["terms"]!.AsArray().Count.ShouldBe(0); } + [Fact] + public async Task Autocomplete_MissingQuery_Returns400() + { + // Query validation happens before the job is looked up, so no fixture is needed. + var response = await ctx.SearchClient.GetAsync("/autocomplete/v1/no/such/id"); + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + } + [Fact] public async Task Autocomplete_MatchingPrefix_ReturnsSuggestions() {