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:
parent
506d1ba761
commit
0c24a4345f
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue