Skip to content

Commit

Permalink
Merge pull request #15 from neuefische/7-add-put-restaurant
Browse files Browse the repository at this point in the history
7 Add Put-route + frontend
  • Loading branch information
gcode-de authored Jun 6, 2024
2 parents f67d56e + 55d7532 commit e4ad9de
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 35 deletions.
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/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

0 comments on commit e4ad9de

Please sign in to comment.