Program can be run with Eclipse very easily, because I’ve added
apply plugin: 'eclipse'
in build.gradle file.
Create model classes, DAO interfaces, services, and add unit tests to components. Determine test coverage using a code coverage tool and ensure your tests cover of at least 60% of your code.
Model classes created are:
Some of them inherit BaseEntity, which is the @MappedSuperClass
that has @Id id
and @Version version
fields, that are
repeated in child classes.
RecipeCategory enum is used in Recipe class as @Enumerated
field.
DAO interfaces are created as Spring-Data CrudRepository
-ies:
All User related DAOs are not exposed to REST API.
FavoriteRecipesDao is interface that allows us to add custom
methods to Spring-Data DAOs. And RecipeDaoImpl is the class that
implements FavoriteRecipesDao and defines manually defined
methods, all of them are relevant to deal with @ManyToMany
relationship between User.favoriteRecipes
, and User.favoriteUsers
Service layer implementations do not have anything in particular, except repeating commands at DAO layer:
Most of them repeat commands at DAO layer with exceptions, that
are relevant for RecipeServiceImpl, because we actually can
access Ingredient
, Item
only through changing
recipe.
There are many methods that were introduced to facilitate request processing in RecipeController.
CustomUserDetailsService implements UserDetailsService
and is very
common service that has loadUserByUsername
method. It is used
through the UserService interface.
Coverage report is in gzipped file called coverage-report.tar.gz
It was generated after I created configuration in Intellijidea with all tests and after running it I saved coverage and gzipped it, because it has too many files. Later I will try to create task in Gradle, but for now that is best I can do.
Using the supplied files, create the template for the recipe list page. Use the following requirements list to ensure all functionality is included in the recipe list page.
Recipes:
-
Displays a list of recipes by name and indicates with a heart icon whether a user has favorited the recipe
-
Allows the user to filter the list by the selected category
-
Allows the user to add a new recipe
-
A user must have an account
-
Allows the user to edit or delete a recipe
-
A user must own the recipe
Displays a list of recipes by name and indicates with a heart icon whether a user has favorited the recipe
Recipes are displayed at the main page with the address
http://localhost:8080/
http://localhost:8080/recipes
http://localhost:8080/recipes/
IndexRedirectController takes care of redirecting from both "/" and "/recipes" pages to "/recipes/" that is mapped in RecipeController. There is probably a better way. But I’ll leave it for now as is. The functionality is tested in IndexRedirectControllerTest
In order to show whether recipe is favorite for user we
generate List<Recipe> favoriteRecipesWithNullsForNonFavorites
that has exactly the same
size as all recipes printed, but on the places where
recipe is favorite for user, it contains recipe, and
elsewhere is null
. This list is passed along with all recipes
to Model
.
The list to be passed is generated in a
RecipeController.generateFavoritesWithNullsForNonFavoritesList
method. And because of that is tested separately in
@Test favoritesWithNonNullsListIsGeneratedCorrectly
in
RecipeControllerTest
Simple list with favorite recipes for users is generated at DAO level at customized RecipeDaoImpl using simple SQL query. It was rather problematic for me to write Spring Query in annotation to some method in RecipeDao, that is why I decided to make custom implementation for now.
The function returning favorite recipes at DAO level is tested in
RecipeDaoTest in findAllFavoritesReturnsOneFavoriteRecipeWithDataLoader
test.
In the REST API for now it is impossible to get favorite recipes
This can be done in many different ways, but I decided to leave it so.
Allows the user to filter the list by the selected category
In the DataLoader for now 5 recipes are added, one for each category. They are all have "tags" that can be clicked and user will be redirected to the index page sorted by selected category tag:
/recipes/?category=name
There is also JavaScript function in app.js file
that is executed when <select>
element is changed. It redirects to index page filtered
by category.
Implementation starts on model layer in RecipeCategory
enum
. I introduced new methods getRecipeCategoryWithHtmlName
that returns RecipeCategory.NONE
or RecipeCategory
found by member htmlName
.
This method is tested in RecipeCategoryTest
allRecipeCategoriesCanBeFoundByName
.
At DAO level I introduced Spring Query method called
findByRecipeCategory
that comes from Spring and returns
List<Recipe>
with RecipeCategory
passed in argument.
This is tested in RecipeDaoTest
listOfRecipesReturnedWhenFindByRecipeCategoryIsCalled
.
At Service layer I introduced in RecipeService and
implemented in RecipeServiceImpl findByRecipeCategoryName
I didn’t test that because it directly returns result of
RecipeDao.findByRecipeCategory
with argument as a result
of RecipeCategory.getRecipeCategoryWithHtmlName
. It may be
not a good idea to put this functionality on Service layer, but
I decided to leave it here.
At Controller layer in RecipeController
filterByCategory
method was introduced, that is passing
to Model.attribute
"recipes" not all recipes, but the
ones recipeService.findByCategoryName
returns. Argument
is query parameter. It comes from JavaScript or from tag anchor,
see Usage.
Also here is "selectedCategory" attribute is added to Model
because we want to display selected option on the redirected
page.
This functionality is tested in RecipeControllerTest
recipesCanBeListedByCategoryOnIndexPage
test.
Allows the user to add a new recipe
New recipe can be added from the home page by pressing "Add Recipe" button.
After pressing that button page /recipes/add-new
is generated.
Template edit.html is used for both /recipes/add-new
and
/recipes/edit/id
, with the difference that new Recipe
object
is passed to Model
when /recipes/add-new
is generated, and
Recipe
from database is passed to Model
when /recipes/edit/id
page is generated.
I made a try to re-use some code by creating
addAttributesToModelForBothEditAndAddNewPages
. Don’t know
whether that was a good idea or not, but I tried.
I also tried to use the same method saveRecipe
for
both adding new recipe POST request and updating
already existing recipe.
A user must have an account
User has an account page at the address
/profile
In order to get to this page, being logged on, user
has to click on his name in the top left corner on
<nav>
element
This is implemented in UserController class, and is tested in UserControllerTest class.
The template rendering this page is called profile.html.
On this page all user’s favorite recipes are displayed. May be later I will include also owned recipes. For now I’ll leave that as TODO.
Allows the user to edit or delete a recipe
User can delete Recipe only if he is owner, or admin.
This is enforced by @PreAuthorize
in RecipeDao and works
both for REST API part and non-REST part.
Unfortunately in order to enforce security upon editing recipe,
I had to use RecipeEventHandler for REST API and
checkIfUserCanEditRecipe
method in RecipeServiceImpl
See the REST Recipes Security and Only Owner Or Admin Can Edit/Update Recipe for more.
A user must own the recipe
When we save recipe we set recipe’s owner.
The responsible field for this relationship is
@ManyToOne Recipe.owner
on the Recipe side,
and @OneToMany User.ownedRecipes
on the User side.
The relationship is determined by foreign_key
owner_id
in the recipes table.
When Recipe is updated, we get the owner from database.
All functionality was moved by me to
service layer in RecipeService.save
method, that
takes care not only about the owner of recipe, but
also sets favorite recipes from database, and
set Recipe.ingredients[i].items
because with the
POST request we set only item id-s.
Related tests can be found in Mock-test in RecipeServiceTest class:
-
savingNewRecipeSetsOwner
-
updatingRecipeDoesNotChangeOwner
Integration tests in RecipeControllerItTest are checking recipe owner consistency as well. See tests:
-
updatingRecipeWithAllValidFieldsWorks
-
savingNewRecipeWithAllValidFieldsWorks
-
deletingRecipeShouldBePossible
Using the supplied files, create the template for the recipe detail page. Use the following requirements list to ensure all functionality is included in the recipe detail page.
-
Only Owner And Admin Can Edit And/Or Update Recipe
Allows a user to add a recipe, or edit the recipe if they are the owner
-
Allows a user to provide a recipe name, description, category (from a list of values), prep time, and cook time
-
Allows a user to provide a list of ingredients. Each ingredient includes an item, condition, and quantity
-
Allows a user to provide a list of steps. Each step includes a description
-
User Can Add/Remove Recipe to/from Favorites
Any user can add the recipe to their favorites
Allows a user to add a recipe, or edit the recipe if they are the owner
In order to introduce security here, additional method that
throws AccessDeniedException
was introduced in Service layer,
in RecipeServiceImpl:
checkIfUserCanEditRecipe
This method is used in saveRecipe
processing POST request
to add/update Recipe
and in editRecipePage
that is
responsible to GET request that is sent when user wants
to see "edit" recipe page.
The sole purpose of this method as it follows from name is to check is user is admin or owner. If he is not, than exception is thrown.
The functionality tested on all levels:
-
-
permissionDeniedIsThrownWhenUserIsNonOwnerNonAdmin
-
-
-
nonOwnerNonAdminCannotAccessEditRecipePage
-
nonOwnerNonAdminCannotUpdateRecipe
-
In RecipeControllerItTest only positive test is done, i.e. for now all the updates are made with logged owner user.
Allows a user to provide a recipe name, description, category (from a list of values), prep time, and cook time Under construction.
All fields that were provided in template files can be filled
by user. I used @NotNull
and @NotEmpty
annotation for all
of them for simplicity.
When one will press 'Add Recipe' button at Home Page and will be redirect to page with adding new recipe.
In order to see that fields are all required, one can press submit button, and see how all of them turn red.
I tested the validation errors only in
"Integration" RecipeControllerItTest. The problem emerged,
because Thymeleaf did not set Recipe
for each recipe.ingredients
and recipe.steps
. That is why we had to remove @Valid
as
annotation from arguments of saveRecipe
controller method,
and before validation, set missing relationships
(see RecipeController saveRecipe
for more).
It is probably possible to test the functionality in "mock"
RecipeControllerTest, but I need to somehow to @Mock
Validator
manually, and I yet don’t know how to do it.
The Validation errors are tested in RecipeControllerItTest in following test:
-
updatingRecipeWithAllNullInvalidFieldsShouldGiveThatNumberOfErrors
-
addingRecipeWithAllEmptyInvalidFieldsShouldGiveThatNumberOfErrors
Allows a user to provide a list of ingredients. Each ingredient includes an item, condition, and quantity
In order to add Ingredients I used JavaScript. All functionality
that is bound to 'Add Ingredient' button can be found in app.js
file, in function that is bound to #add-another-ingredient-button
and executed upon click.
Simple Jquery is used all over the place and in sudo code can be described as following
-
get last
<div class="ingredient-row">
-
get
id
from thatdivWithLastIngredient
-
increment
id
-
clone
divWithLastIngredient
-
add clone after
divWithLastIngredient
-
change
id
andname
attributes for<select>
element that selectsingredient.item.id
-
change
id
andname
attributes for<input>
elements foringredient.condition
andingredient.quantity
In the end the purpose of JavaScript is from this <div>
:
<div class="ingredient-row">
<input hidden=""
type="text"
id="ingredients0.id"
name="ingredients[0].id"
value="">
<input
hidden=""
type="text"
id="ingredients0.version"
name="ingredients[0].version"
value="">
<div class="prefix-20 grid-30">
<p>
<select id="ingredients0.item.id" name="ingredients[0].item.id">
<option value="0">Select Item</option>
<option value="1">item 1</option>
<option value="2">item 2</option>
</select>
</p>
</div>
<div class="grid-30">
<p>
<input
type="text"
id="ingredients0.condition"
name="ingredients[0].condition"
value="">
</p>
</div>
<div class="grid-10 suffix-10">
<p>
<input
type="text"
id="ingredients0.quantity"
name="ingredients[0].quantity"
value="">
</p>
</div>
<div class="clear"></div>
</div>
Create new <div>
where:
ingredient.quantity
-
id="ingredients0.quantity"
-
name="ingredients[0].quantity"
ingredient.condition
-
id="ingredients0.condition"
-
name="ingredients[0].condition"
ingredient.item.id
-
id="ingredients0.item.id"
-
name="ingredients[0].item.id"
Will be changed respectively to:
ingredient.quantity
-
id="ingredients1.quantity"
-
name="ingredients[1].quantity"
ingredient.condition
-
id="ingredients1.condition"
-
name="ingredients[1].condition"
ingredient.item.id
-
id="ingredients1.item.id"
-
name="ingredients[1].item.id"
Of course id
of last ingredient is taken from divWithLastIngredient
.
When we add new Recipe, we generated "add new" page with one
Ingredient
and one Step
, so that cloning works.
The new div will be without hidden
ingredient.version
and
ingredient.id
, because this fields should be left null when we add new
Ingredient
.
The resulting <div>
with new Ingredient made from example
above should look like this:
<div class="ingredient-row">
<div class="prefix-20 grid-30">
<p>
<select id="ingredients1.item.id" name="ingredients[1].item.id">
<option value="0">Select Item</option>
<option value="1">item 1</option>
<option value="2">item 2</option>
</select>
</p>
</div>
<div class="grid-30">
<p>
<input
type="text"
id="ingredients1.condition"
name="ingredients[1].condition"
value="">
</p>
</div>
<div class="grid-10 suffix-10">
<p>
<input
type="text"
id="ingredients1.quantity"
name="ingredients[1].quantity"
value="">
</p>
</div>
<div class="clear"></div>
</div>
One should definitely test this somehow, hopefully I’ll get to JavaScript Unit Testing someday.
Allows a user to provide a list of steps. Each step includes a description
The way "Add New Step" button works on "edit" recipe page, is the
same as ingredient. It is also defined in app.js file using
Jquery that is tracking the click on button with
id="add-another-step-button"
.
Upon click in the same manner new <div>
with new Step
is added in a bit different and easier manner, because Step
has only one field: itself description
:
-
get last
<div class="step-row">
-
get
id
or index of laststep
at page -
increment
id
-
create new
div
with newid
andname
attributes -
insert new
div
afterdivWithLastStep
So if initial div in blank "add new" recipe page looks like
<div class="step-row">
<div class="prefix-20 grid-80">
<p>
<input
id="steps0"
name="steps[0]"
value="">
</p>
</div>
</div>
Then according new div
with new Step
should look like:
<div class="step-row">
<div class="prefix-20 grid-80">
<p>
<input
id="steps1"
name="steps[1]"
value="">
</p>
</div>
</div>
At DAO level in RecipeDaoImpl class following to methods were introduced:
-
addFavoriteRecipeForUser
-
removeFavoriteRecipeForUser
Because relationship between User.favoriteRecipes
and Recipe.favoriteUsers
is of type @ManyToMany
, all we need to do is to remove respective
links in @JoinTable
with name 'users_favorite_recipes'
That is exactly what is done in both methods with following simple SQL queries.
-- add to favorites query
INSERT INTO users_favorite_recipes
(recipe_id, user_id)
VALUES (?, ?)
-- remove from favorites query
DELETE FROM users_favorite_recipes
WHERE recipe_id = ? AND user_id = ?
It could’ve been done with easier @NativeQuery
, but well,
I found that pretty late, so this later can be refactored.
The functionality tested at DAO level with the following tests in RecipeDaoTest:
-
recipeCanBeAddedToFavorites
-
recipeCanBeRemovedFromFavorites
At Service Layer we decided to take into account the check if recipe is favorite for user or not, and based on that update favorite status of recipe.
That is why in RecipeServiceImpl two new methods were introduced:
-
updateFavoriteRecipesForUser
-
checkIfRecipeIsFavoriteForUser
The last one simply checks and returns true
if
recipe is favorite and false
otherwise.
The first one uses the last one, and depending on that calls respective method in DAO layer, that adds or removes recipe from favorites
The functionality is tested in RecipeServiceTest:
-
updatingRecipeWhenRecipeIsAlreadyFavoriteShouldRemoveRecipeFromFavorites
-
updatingRecipeWhenRecipeIsNotFavoriteShouldAddRecipeToFavorites
-
trueIsReturnedWhenRecipeIsFavoriteForCheckIfFavoriteMethod
-
falseIsReturnedWhenRecipeIsNotFavoriteForCheckIfFavoriteMethod
Finally in RecipeController we introduced updateFavoriteStatusOfRecipe
method that is responsible for inline form POST request,
that updates favorite recipe for user, using method introduced in
RecipeServiceImpl and sets flash message depending whether the
recipe was removed or added from favorites.
In the 'detail' recipe page, detailRecipePage
method,
we use introduced in Service Layer
method checkIfRecipeIsFavoriteForUser
to pass proper
heart icon, filled or empty and proper button name
changing favorite status of recipe.
The following tests were added in "mock" RecipeControllerTest:
-
detailRecipePageWithFavoriteRecipeShouldRenderSuccessfully
-
detailRecipePageWithNonFavoriteRecipeShouldRenderSuccessfully
-
userCanAddRecipeToFavoritesFromDetailPage
-
userCanRemoveRecipeFromFavoritesFromDetailPage
The recipe list page should have a search feature. A user can enter a search term and the recipe list will display results that have the search phrase in the description.
At RecipeDao interface Spring "query" method
findByDescriptionContaining
was added, that does all the hard work
for us.
The functionality is tested just in case in RecipeDaoTest
-
recipeCanBeSearchedByDescription
At RecipeServiceImpl class defined DAO method without no changes is used. And I decided not to test this.
At RecipeController class filterByDescription
method is introduced,
with one @RequestParam
"description". It looks very similar to
filterByCategory
method. I’m glad I’ve decided to re-use that
"circular" fillModelWithRecipesFavoritesAndCategories
method.
The functionality is tested in RecipeControllerTest:
-
recipesCanBeSearchedByDescriptionOnIndexPage
In actual index.html template inline <form>
was introduced
with GET method and simple one parameter "description". So that
when user presses button "Search" recipes are searched by
"description" provided.
As a future TODO we definitely will be doing combined search, but only after submission of the project. It’s been two months now. And it is time to finish this.
Enable user authentication with Spring Security. Use the supplied files to create templates for login page, registration page, and profile page. You must build the registration component, as it does not come with Spring Security. Create necessary controllers, services, and DAO to add a new user. Make sure to include validation so that a user may not use a username that already exists. Check out links in external resources if you get stuck.
User authentication is enabled in SecurityConfig class.
It enables @PreAuthorize
and other annotations, as well as
password encoder.
In configure(WebSecurity web)
method we setup ignoring everything that is in
src/main/resources/static/assets
, i.e. all images, CSS and
JavaScript.
In configure(HttpSecurity http)
we permit user
before logging in, access only SIGN_UP_PAGE
and
LOGIN_PAGE
.
loginSuccessHandler
redirects user to RECIPES_HOME_PAGE
,
whereas loginFailureHandler
creates flash message that
attaches FlashMessage with FAILURE
status.
LoginController class is created in order to set up
-
LOGIN_PAGE
inloginForm
method -
SIGN_UP_PAGE
insignUpPage
method -
SIGN_UP_PAGE
POST request inregisterNewUser
method.
I don’t exactly know how loginForm
works. But
I know that it sets "flash" from session if user types
wrong password. If however, user register valid user
on SIGN_UP_PAGE
, then he’ll be redirected to this
page with successful flash to log in.
SIGN_UP_PAGE
simply loads new UserDto object into
Model
, and if user made mistakes when creating new
user, then he is redirected back with all user
parameters except password
and matchingPassword
registerNewUser
processes POST request of creating
new user.
Idea of UserDto came from here.
It is first of all needed because we set password
indirectly and cannot use validation annotation
on User.password
field directly. Also nice thing about
this approach that we created our own PasswordMatches
annotation that uses PasswordMatchesValidator
and checks if passwords match. Whereas we cannot simply
introduce new matchingPassword
field in User class.
One has to note here on userDto.password
field regex
matcher is used taken from here.
The regex is tested separately in the UserDtoTest class.
One has also note here, that in order to ensure more
security and not sending password back and forth, i.e.
to increase user experience I’ve added in app.js file
JavaScript Jquery function checking whether passwords in
SIGN_UP_PAGE
match, and whether they are strong or not.
The most of the code was taken from
here.
The button "sign-up" sending POST request will appear only if both password match and are "strong".
So we won’t see backend check unless we actually disable JavaScript. But I decided to leave them both hanging.
"Check whether the user exists with such username" is done
on service layer in userService.registerNewUser
method.
It throws UserAlreadyExistsException that unfortunately
does not work properly with @ExceptionHandler
that is
why it is explicitly caught in LoginController
registerNewUser
method
The tests checking most of the user registration can be found in following classes:
Create REST endpoints for CRUD operations.
REST API is build with HATEOAS and HAL browser, from the following dependencies in build.gradle:
compile 'org.springframework.boot:spring-boot-starter-data-rest'
compile 'org.springframework.data:spring-data-rest-hal-browser'
In application.properties file spring.data.rest.base-path
is defined, so
that we know upon which address to find out REST API.
In the @Configuration
RestConfig class we add @Validator for
all entities as ValidatingRepositoryEventListener
. Apparently that
is because REST is built from DAO, and without having Service
layer helping out with the stuff, we are using Events and
Listeners.
Validator
itself is defined from LocalValidatorFactoryBean
in
AppConfig class.
One more important class that interacts with REST is
RecipeEventHandler. For now @HandleBeforeCreate
event is
defined there, with which we set-up owner of the Recipe
before saving, and @HandleBeforeSave
as well, by checking that
only owner of Recipe
or admin can update Recipe
.
Below is the list of links REST generates for us to use:
/api/v1/recipes : GET, PUT, POST, DELETE
/api/v1/ingredients : GET, PUT, POST, DELETE
/api/v1/steps : GET, PUT, POST, DELETE
/api/v1/items : GET, PUT, POST, DELETE
URI-s are defined automatically, but I’ve also specified them in WebConstants class, so that we can use them type-safe in tests.
In order to see specific object one has to add id
at the
end.
It is impossible to create Recipe
with Ingredient
in one request unfortunately.
However one can create recipe
with recipe.steps
, because it is @ElementCollection
,
which means List<String>
that is controlled solely by
Recipe
without DAO.
In order to create new Recipe one has to do the following:
-
make POST request to create new
Recipe
withoutIngredient
:{ "id" : null, "version" : null, "name" : "test name", "description" : "test description", "recipeCategory" : "BREAKFAST", "photoUrl" : "test photo url", "preparationTime" : "test prep time", "cookTime" : "test cook time", "ingredients" : [], "steps" : [ "step 1", "step 2" ] }
-
make POST request to create new
Ingredient
withRecipe
andItem
link:{ "id" : "null", "version" : "null", "item" : "http://localhost:8080/api/v1/items/1", "condition" : "condition", "quantity" : "quantity", "recipe" : "http://localhost:8080/api/v1/recipes/1" }
NoteItem
can be created without links easily via POST with:{ "name" : "item" }
PUT
requests can be done in the same manner. So I omit this
section description here
DELETE
requests are done with empty body but with id
of the entity
to be deleted.
Recipe
can be updated only by owner of Recipe
or admin user.
Whether user is admin or owner is checked in RecipeEventHandler class in
checkIfOwnerOrAdminIsEditing
method. Upon authentication error,
CustomAccessDeniedException is thrown that is just like usual
AccessDeniedException
provided by Spring, but this custom
can be tested. When we throw AccessDeniedException
then
in tests NestedServletException
is thrown, that complicates
test checks.
Recipe
deletion is managed by @PreAuthorize
Spring Security
Expression, that is enabled in SecurityConfig class. The idea
here is naturally same: only owner of Recipe
or admin user
can delete Recipe.
Mostly REST API is tested with following integration tests: