Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

7 Add Put-route + frontend #15

Merged
merged 9 commits into from
Jun 6, 2024
10 changes: 6 additions & 4 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo.spring3x</artifactId>
<version>4.13.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.neuefische.team2.backend.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.neuefische.team2.backend.restaurant;

import com.neuefische.team2.backend.restaurant.domain.NewRestaurantDTO;
import com.neuefische.team2.backend.exceptions.ResourceNotFoundException;
import com.neuefische.team2.backend.restaurant.domain.NewRestaurantDTO;
import com.neuefische.team2.backend.restaurant.domain.Restaurant;
import jakarta.validation.Valid;
Expand All @@ -9,6 +11,8 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;

import java.util.List;

Expand All @@ -23,7 +27,7 @@ public RestaurantController(RestaurantService restaurantService) {
}

@GetMapping
public List<Restaurant> getRestaurants() {
List<Restaurant> getRestaurants() {
return restaurantService.getRestaurants();
}

Expand All @@ -38,4 +42,9 @@ public Restaurant addRestaurant(@RequestBody @Valid NewRestaurantDTO newRestaura
Restaurant restaurant = new Restaurant(null, newRestaurantDTO.title(), newRestaurantDTO.city());
return restaurantService.addRestaurant(restaurant);
}

@PutMapping("{id}")
Restaurant putRestaurant(@Valid @RequestBody NewRestaurantDTO newRestaurantDTO, @PathVariable String id) throws ResourceNotFoundException {
return restaurantService.updateRestaurant(newRestaurantDTO, id);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.neuefische.team2.backend.restaurant;

import com.neuefische.team2.backend.exceptions.ResourceNotFoundException;
import com.neuefische.team2.backend.restaurant.domain.NewRestaurantDTO;
import com.neuefische.team2.backend.exceptions.NoSuchRestaurantException;
import com.neuefische.team2.backend.restaurant.domain.Restaurant;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class RestaurantService {
Expand All @@ -25,4 +29,14 @@ public Restaurant findRestaurantById(String id) {
public Restaurant addRestaurant(Restaurant restaurant) {
return restaurantRepository.save(restaurant);
}

public Restaurant updateRestaurant(NewRestaurantDTO updatedRestaurantDTO, String id) throws ResourceNotFoundException {
Optional<Restaurant> existingRestaurant = restaurantRepository.findById(id);
if (existingRestaurant.isPresent()) {
Restaurant updatedRestaurant = new Restaurant(id, updatedRestaurantDTO.title().trim(), updatedRestaurantDTO.city().trim());
return restaurantRepository.save(updatedRestaurant);
} else {
throw new ResourceNotFoundException("Restaurant not found with id " + id);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.neuefische.team2.backend.restaurant.domain;

import org.springframework.data.annotation.Id;
import jakarta.validation.constraints.NotBlank;
import org.springframework.data.mongodb.core.mapping.Document;

@Document("restaurants")
public record Restaurant(
@Id
String id,

@NotBlank(message = "Restaurant-Titel muss vorhanden sein.")
String title,

@NotBlank(message = "Restaurant-Stadt muss vorhanden sein.")
String city
) {
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.neuefische.team2.backend.restaurant;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.neuefische.team2.backend.restaurant.domain.NewRestaurantDTO;
import com.neuefische.team2.backend.restaurant.domain.Restaurant;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.neuefische.team2.backend.restaurant.domain.Restaurant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
Expand All @@ -18,6 +22,8 @@
@AutoConfigureMockMvc
class RestaurantControllerIntegrationTest {

ObjectMapper objectMapper = new ObjectMapper();

@Autowired
private MockMvc mockMvc;

Expand Down Expand Up @@ -218,4 +224,38 @@ void getRestaurantById_whenRestaurantDoesNotExist_thenReturnNotFound() throws Ex



// TODO: Write a test to receive one restaurant as soon as POST endpoint is implemented.


@Test
void updateRestaurant_whenRestaurantExists_thenReturnUpdatedRestaurant() throws Exception {
// Arrange: Add a restaurant to the DB
NewRestaurantDTO newRestaurant = new NewRestaurantDTO("Old Name", "Old City");
String responseContent = mockMvc.perform(MockMvcRequestBuilders.post("/api/restaurants")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(newRestaurant)))
.andExpect(MockMvcResultMatchers.status().isCreated())
.andReturn().getResponse().getContentAsString();

Restaurant createdRestaurant = objectMapper.readValue(responseContent, Restaurant.class);

// Act: Update the restaurant
NewRestaurantDTO updatedRestaurant = new NewRestaurantDTO("New Name", "New City");
mockMvc.perform(MockMvcRequestBuilders.put("/api/restaurants/" + createdRestaurant.id())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updatedRestaurant)))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.title").value("New Name"))
.andExpect(MockMvcResultMatchers.jsonPath("$.city").value("New City"));
}

@Test
void updateRestaurant_whenRestaurantDoesNotExist_thenReturnNotFound() throws Exception {
// Act: Try to update a non-existing restaurant
Restaurant updatedRestaurant = new Restaurant(null, "New Name", "New City");
mockMvc.perform(MockMvcRequestBuilders.put("/api/restaurants/999")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updatedRestaurant)))
.andExpect(MockMvcResultMatchers.status().isNotFound());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.neuefische.team2.backend.restaurant;

import com.neuefische.team2.backend.exceptions.ResourceNotFoundException;
import com.neuefische.team2.backend.restaurant.domain.NewRestaurantDTO;
import com.neuefische.team2.backend.exceptions.NoSuchRestaurantException;
import com.neuefische.team2.backend.restaurant.domain.Restaurant;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -30,7 +32,7 @@ void getRestaurants_whenNoRestaurantsInDB_thenReturnEmptyList() {
}

@Test
void getRestaurants_whenOneRestaurantInDB_thenReturnListOfOne() {
void getRestaurants_whenOneRestaurantsInDB_thenReturnListOfOne() {
//GIVEN
Restaurant restaurant = new Restaurant("1", "The Mockingbird", "New York");
when(mockRestaurantRepository.findAll()).thenReturn(Collections.singletonList(restaurant));
Expand All @@ -43,6 +45,41 @@ void getRestaurants_whenOneRestaurantInDB_thenReturnListOfOne() {
List<Restaurant> expected = List.of(restaurant);
assertEquals(expected, actual);
}

@Test
void updateRestaurant_whenRestaurantExists_thenUpdateAndReturnRestaurant() {
//GIVEN
Restaurant existingRestaurant = new Restaurant("1", "Old Name", "Old City");
NewRestaurantDTO updatedRestaurantData = new NewRestaurantDTO("New Name", "New City");
Restaurant updatedRestaurant = new Restaurant("1", "New Name", "New City");

when(mockRestaurantRepository.findById("1")).thenReturn(Optional.of(existingRestaurant));
when(mockRestaurantRepository.save(any(Restaurant.class))).thenReturn(updatedRestaurant);

// WHEN
Restaurant actual = restaurantService.updateRestaurant(updatedRestaurantData, "1");

//THEN
verify(mockRestaurantRepository).findById("1");
verify(mockRestaurantRepository).save(any(Restaurant.class));
assertEquals(updatedRestaurant, actual);
}

@Test
void updateRestaurant_whenRestaurantDoesNotExist_thenThrowResourceNotFoundException() {
//GIVEN
NewRestaurantDTO updatedRestaurantData = new NewRestaurantDTO("New Name", "New City");

when(mockRestaurantRepository.findById("1")).thenReturn(Optional.empty());

// WHEN / THEN
assertThrows(ResourceNotFoundException.class, () -> {
restaurantService.updateRestaurant(updatedRestaurantData, "1");
});

verify(mockRestaurantRepository).findById("1");
verify(mockRestaurantRepository, never()).save(any(Restaurant.class));
}
@Test
void findRestaurantById_whenRestaurantExists_thenReturnRestaurant() {
//GIVEN
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import RestaurantsPage from "./pages/RestaurantsPage.tsx";
import RestaurantDetailsPage from "./pages/RestaurantDetailsPage.tsx";
import {Route, Routes} from "react-router-dom";
import AddRestaurantsPage from "./pages/AddRestaurantsPage.tsx";
import RestaurantEditPage from "./pages/RestaurantEditPage.tsx";
import './App.css'

function App() {
Expand All @@ -10,6 +11,7 @@ function App() {
<Routes>
<Route path="/" element={<RestaurantsPage />}/>
<Route path="/restaurants/add" element={<AddRestaurantsPage />}/>
<Route path="/restaurants/edit/:id" element={<RestaurantEditPage />}/>
<Route path="/restaurants/:id" element={<RestaurantDetailsPage />}/>
</Routes>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {MemoryRouter} from "react-router-dom";
test('RestaurantForm component displays label "title"', () => {
render(
<MemoryRouter>
<RestaurantForm />
<RestaurantForm onSubmit={jest.fn()} restaurantData={null}/>
</MemoryRouter>);
const titleInput = screen.getByLabelText("Title");
expect(titleInput).toBeInTheDocument();
Expand All @@ -15,7 +15,7 @@ test('RestaurantForm component displays label "title"', () => {
test('RestaurantForm component displays input field "title"', () => {
render(
<MemoryRouter>
<RestaurantForm />
<RestaurantForm onSubmit={jest.fn()} restaurantData={null}/>
</MemoryRouter>);
const titleInput = screen.getByRole("textbox", {
name: /title/i
Expand All @@ -26,7 +26,7 @@ test('RestaurantForm component displays input field "title"', () => {
test('RestaurantForm component displays label "city"', () => {
render(
<MemoryRouter>
<RestaurantForm />
<RestaurantForm onSubmit={jest.fn()} restaurantData={null}/>
</MemoryRouter>);
const titleLabel = screen.getByLabelText("City");
expect(titleLabel).toBeInTheDocument();
Expand All @@ -35,22 +35,22 @@ test('RestaurantForm component displays label "city"', () => {
test('RestaurantForm component displays input field "city"', () => {
render(
<MemoryRouter>
<RestaurantForm />
<RestaurantForm onSubmit={jest.fn()} restaurantData={null}/>
</MemoryRouter>);
const cityInput = screen.getByRole("textbox", {
name: /city/i
});
expect(cityInput).toBeInTheDocument();
});

test('RestaurantForm component displays "add" button', () => {
test('RestaurantForm component displays "save" button', () => {
render(
<MemoryRouter>
<RestaurantForm />
<RestaurantForm onSubmit={jest.fn()} restaurantData={null}/>
</MemoryRouter>);
const addButton = screen.getByRole("button", {
name: /add/i
const saveButton = screen.getByRole("button", {
name: /save/i
});
expect(addButton).toBeInTheDocument();
expect(saveButton).toBeInTheDocument();
});

35 changes: 16 additions & 19 deletions frontend/src/components/RestaurantForm/RestaurantForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {ChangeEvent, FormEvent, useState} from "react";
import {NewRestaurantDTOType} from "../../model/Restaurant.ts";
import axios from "axios";
import {useNavigate} from "react-router-dom";
import {ChangeEvent, useState} from "react";
import {NewRestaurantDTOType, RestaurantType} from "../../model/Restaurant.ts";
import {
StyledFieldError,
StyledForm,
Expand All @@ -10,16 +8,21 @@ import {
StyledInputField
} from "./RestaurantForm.styled.ts";

export default function RestaurantForm() {
type RestaurantFormProps ={
restaurantData: RestaurantType | null;
onSubmit: (rg0: NewRestaurantDTOType) => void;
}

export default function RestaurantForm({restaurantData, onSubmit}:RestaurantFormProps) {

const initialFieldValidation = {
title: "",
city: ""
}

const [formData, setFormData] = useState<NewRestaurantDTOType>({title: "", city: ""});
const [formData, setFormData] = useState<NewRestaurantDTOType>( restaurantData || {title: "", city: ""});
const [fieldValidation, setFieldValidation] = useState<NewRestaurantDTOType>(initialFieldValidation);
const navigate = useNavigate();


function handleUserInput(event: ChangeEvent<HTMLInputElement>) {
setFormData({...formData, [event.target.name]: event.target.value});
Expand All @@ -31,19 +34,13 @@ export default function RestaurantForm() {
}
}

function handleAddRestaurant(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
axios.post("/api/restaurants", formData)
.then(() => {
navigate("/")
})
.catch((error) => {
window.console.error(error.message)
})
}


return (
<StyledForm onSubmit={handleAddRestaurant}>
<StyledForm onSubmit={(event )=>{
event.preventDefault();
onSubmit(formData);
}}>
<StyledFormBody>
<StyledFormRow>
<label htmlFor="title">Title</label>
Expand All @@ -70,7 +67,7 @@ export default function RestaurantForm() {
<StyledFieldError>{fieldValidation.city}</StyledFieldError>
</StyledFormRow>
</StyledFormBody>
<button type="submit">Add</button>
<button type="submit">Save</button>
</StyledForm>
)
}
Loading
Loading