fix: add /filters endpoint to resolve 500 error on GET /supplements/filters

Added GET /api/v1/supplements/filters returning distinct categories,
brands, and countries. Placed before /{id} mapping so Spring MVC
matches the exact path first. Previously "filters" was parsed as UUID,
causing IllegalArgumentException -> 500.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Backend Agent 2026-04-12 14:23:32 +00:00
parent 506d1ba761
commit 0c24a4345f
7 changed files with 90 additions and 0 deletions

View File

@ -47,6 +47,12 @@ public class SupplementController {
return supplementService.findAll(pageable); return supplementService.findAll(pageable);
} }
@GetMapping("/filters")
@Operation(summary = "Get available filter values (categories, brands, countries)")
public FilterResponse getFilters() {
return supplementService.getFilters();
}
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "Get supplement by ID") @Operation(summary = "Get supplement by ID")
public SupplementResponse getById(@PathVariable UUID id) { public SupplementResponse getById(@PathVariable UUID id) {

View File

@ -0,0 +1,10 @@
package ru.oa2.mvp.nutriapi.dto;
import java.util.List;
public record FilterResponse(
List<String> categories,
List<String> brands,
List<String> countries
) {
}

View File

@ -8,11 +8,21 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.oa2.mvp.nutriapi.entity.Supplement; import ru.oa2.mvp.nutriapi.entity.Supplement;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Repository @Repository
public interface SupplementRepository extends JpaRepository<Supplement, UUID> { public interface SupplementRepository extends JpaRepository<Supplement, UUID> {
@Query("SELECT DISTINCT s.category FROM Supplement s WHERE s.category IS NOT NULL ORDER BY s.category")
List<String> findDistinctCategories();
@Query("SELECT DISTINCT s.brand FROM Supplement s WHERE s.brand IS NOT NULL ORDER BY s.brand")
List<String> findDistinctBrands();
@Query("SELECT DISTINCT s.country FROM Supplement s WHERE s.country IS NOT NULL ORDER BY s.country")
List<String> findDistinctCountries();
Page<Supplement> findByCategory(String category, Pageable pageable); Page<Supplement> findByCategory(String category, Pageable pageable);
Page<Supplement> findByBrand(String brand, Pageable pageable); Page<Supplement> findByBrand(String brand, Pageable pageable);

View File

@ -11,6 +11,7 @@ import ru.oa2.mvp.nutriapi.entity.Supplement;
import ru.oa2.mvp.nutriapi.exception.ResourceNotFoundException; import ru.oa2.mvp.nutriapi.exception.ResourceNotFoundException;
import ru.oa2.mvp.nutriapi.repository.SupplementRepository; import ru.oa2.mvp.nutriapi.repository.SupplementRepository;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Service @Service
@ -46,6 +47,14 @@ public class SupplementService {
return toPageResponse(page); return toPageResponse(page);
} }
@Transactional(readOnly = true)
public FilterResponse getFilters() {
List<String> categories = supplementRepository.findDistinctCategories();
List<String> brands = supplementRepository.findDistinctBrands();
List<String> countries = supplementRepository.findDistinctCountries();
return new FilterResponse(categories, brands, countries);
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public PageResponse<SupplementResponse> search(String query, Pageable pageable) { public PageResponse<SupplementResponse> search(String query, Pageable pageable) {
Page<Supplement> page = supplementRepository.fullTextSearch(query, pageable); Page<Supplement> page = supplementRepository.fullTextSearch(query, pageable);

View File

@ -1,15 +1,18 @@
package ru.oa2.mvp.nutriapi; package ru.oa2.mvp.nutriapi;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest @SpringBootTest
@Testcontainers @Testcontainers
@EnabledIf("isDockerAvailable")
class NutriApiApplicationTests { class NutriApiApplicationTests {
@Container @Container
@ -25,6 +28,15 @@ class NutriApiApplicationTests {
registry.add("spring.datasource.password", postgres::getPassword); registry.add("spring.datasource.password", postgres::getPassword);
} }
static boolean isDockerAvailable() {
try {
DockerClientFactory.instance().client();
return true;
} catch (Exception e) {
return false;
}
}
@Test @Test
void contextLoads() { void contextLoads() {
} }

View File

@ -159,4 +159,34 @@ class SupplementControllerTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.content").isArray()); .andExpect(jsonPath("$.content").isArray());
} }
@Test
void getFilters_returnsFilterValues() throws Exception {
var filters = new FilterResponse(
List.of("Minerals", "Vitamins"),
List.of("BrandA", "BrandB"),
List.of("France", "USA")
);
when(supplementService.getFilters()).thenReturn(filters);
mockMvc.perform(get("/api/v1/supplements/filters"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.categories").isArray())
.andExpect(jsonPath("$.categories[0]").value("Minerals"))
.andExpect(jsonPath("$.categories[1]").value("Vitamins"))
.andExpect(jsonPath("$.brands[0]").value("BrandA"))
.andExpect(jsonPath("$.countries[0]").value("France"));
}
@Test
void getFilters_returnsEmptyArraysWhenNoData() throws Exception {
var filters = new FilterResponse(List.of(), List.of(), List.of());
when(supplementService.getFilters()).thenReturn(filters);
mockMvc.perform(get("/api/v1/supplements/filters"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.categories").isEmpty())
.andExpect(jsonPath("$.brands").isEmpty())
.andExpect(jsonPath("$.countries").isEmpty());
}
} }

View File

@ -177,4 +177,17 @@ class SupplementServiceTest {
assertThat(result.content()).hasSize(1); assertThat(result.content()).hasSize(1);
} }
@Test
void getFilters_returnsDistinctValues() {
when(supplementRepository.findDistinctCategories()).thenReturn(List.of("Minerals", "Vitamins"));
when(supplementRepository.findDistinctBrands()).thenReturn(List.of("BrandA"));
when(supplementRepository.findDistinctCountries()).thenReturn(List.of("USA"));
var result = supplementService.getFilters();
assertThat(result.categories()).containsExactly("Minerals", "Vitamins");
assertThat(result.brands()).containsExactly("BrandA");
assertThat(result.countries()).containsExactly("USA");
}
} }