Initial commit

parents
This diff is collapsed.
DAAExample
==========
Aplicación y arquitectura de ejemplo para la asignatura Desarrollo Ágil de
Aplicaciones del Grado en Ingeniería Informática de la Escuela Superior de
Ingeniería Informática de la Universidad de Vigo.
## Ejecución con Maven
La configuración de Maven ha sido preparada para permitir varios tipos de
ejecución.
### Ejecución de la aplicación con Tomcat y MySQL
El proyecto está configurado para poder ejecutar la aplicación sin tener que
realizar ninguna configuración adicional salvo tener disponible un servidor
MySQL en local.
Los ficheros del proyecto `db/mysql.sql` y 'db/mysql-with-inserts.sql' contienen
todas las consultas necesarias para crear la base de datos y el usuario
requeridos, con o sin datos de ejemplo, respectivamente. Por lo tanto, podemos
configurar inicialmente la base de datos con cualquiera de los siguientes
comandos (desde la raíz el proyecto):
* Sin datos: `mysql -u root -p < db/mysql.sql`
* Con datos: `mysql -u root -p < db/mysql-with-inserts.sql`
Una vez configurada la base de datos podemos lanzar la ejecución con el comando:
`mvn -Prun-tomcat-mysql -DskipTests=true package cargo:run`
La aplicación se servirá en la URL local: http://localhost:9080/DAAExample
Para detener la ejecución podemos utilizar `Ctrl+C`.
### Ejecución de la aplicación con Tomcat y MySQL con redespliegue automático
Durante el desarrollo es interesante que la apliación se redespliegue de forma
automática cada vez que se hace un cambio. Para ello podemos utilizar el
siguiente comand:
`mvn -Prun-tomcat-mysql -DskipTests=true package cargo:start fizzed-watcher:run`
La aplicación se servirá en la URL local: http://localhost:9080/DAAExample
Para detener la ejecución podemos utilizar `Ctrl+C`.
### Construcción con tests de unidad e integración
En esta construcción se ejecutarán todos los tests relacionados con el backend:
* **Unidad**: se utilizan para testear las entidades y las capas DAO y REST de
forma aislada.
* **Integración**: se utilizan para testear las capas REST y DAO de forma
integrada. Para este tipo de pruebas se utiliza una base de datos HSQL en
memoria.
El comando para lanzar esta construcción es:
`mvn install`
### Construcción con tests de unidad, integración y aceptación
Esta construcción es similar a la previa, añadiendo las **pruebas de
aceptación**, que comprueban que las fucionalidades de la aplicación están
correctamente implementadas.
En estas pruebas se descarga y arranca el un servidor Tomcat 8 en el que se
despliega la aplicación configurada para utilizar una base de datos HSQL. Las
pruebas se hacen sobre la interfaz web con Selenium, que iniciará un Firefox
local de forma automática.
`mvn -Pacceptance-tests-cargo install`
CREATE DATABASE `daaexample`;
CREATE TABLE `daaexample`.`people` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`surname` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `daaexample`.`users` (
`login` varchar(100) NOT NULL,
`password` varchar(64) NOT NULL,
PRIMARY KEY (`login`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
GRANT ALL ON `daaexample`.* TO 'daa'@'localhost' IDENTIFIED BY 'daa';
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Antón','Pérez');
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Manuel','Martínez');
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Laura','Reboredo');
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Perico','Palotes');
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Ana','María');
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'María','Nuevo');
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Alba','Fernández');
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Asunción','Jiménez');
-- The password for each user is its login suffixed with "pass". For example, user "admin" has the password "adminpass".
INSERT INTO `daaexample`.`users` (`login`,`password`) VALUES ('admin', '43f413b773f7d0cfad0e8e6529ec1249ce71e8697919eab30d82d800a3986b70');
INSERT INTO `daaexample`.`users` (`login`,`password`) VALUES ('normal', '688f21dd2d65970f174e2c9d35159250a8a23e27585452683db8c5d10b586336');
CREATE DATABASE `daaexample`;
CREATE TABLE `daaexample`.`people` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`surname` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `daaexample`.`users` (
`login` varchar(100) NOT NULL,
`password` varchar(64) NOT NULL,
PRIMARY KEY (`login`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
GRANT ALL ON `daaexample`.* TO 'daa'@'localhost' IDENTIFIED BY 'daa';
This diff is collapsed.
package es.uvigo.esei.daa;
import static java.util.stream.Collectors.toSet;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import es.uvigo.esei.daa.rest.PeopleResource;
import es.uvigo.esei.daa.rest.UsersResource;
/**
* Configuration of the REST application. This class includes the resources and
* configuration parameter used in the REST API of the application.
*
* @author Miguel Reboiro Jato
*
*/
@ApplicationPath("/rest/*")
public class DAAExampleApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return Stream.of(
PeopleResource.class,
UsersResource.class
).collect(toSet());
}
@Override
public Map<String, Object> getProperties() {
// Activates JSON automatic conversion in JAX-RS
return Collections.singletonMap(
"com.sun.jersey.api.json.POJOMappingFeature", true
);
}
}
package es.uvigo.esei.daa;
import static java.util.Objects.requireNonNull;
import java.io.IOException;
import java.util.Base64;
import java.util.Optional;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import es.uvigo.esei.daa.dao.DAOException;
import es.uvigo.esei.daa.dao.UsersDAO;
/**
* Security filter that implements a login protocol based on the HTTP Basic
* Authentication protocol. In this case, the login and password can be provided
* as plain request parameters or as a cookie named "token" that should contain
* both values in the same format as HTTP Basic Authentication
* ({@code base64(login + ":" + password)}).
*
* @author Miguel Reboiro Jato
*
*/
@WebFilter(urlPatterns = "/*")
public class LoginFilter implements Filter {
private static final String LOGOUT_PATH = "/logout";
private static final String REST_PATH = "/rest";
private static final String[] PUBLIC_PATHS = new String[] {
"/index.html", "/js", "/css" // Add the paths that can be publicly accessed (e.g. /images...)
};
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
final HttpServletRequest httpRequest = (HttpServletRequest) request;
final HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
if (isLogoutPath(httpRequest)) {
destroySession(httpRequest);
removeTokenCookie(httpRequest, httpResponse);
redirectToIndex(httpRequest, httpResponse);
} else if (isPublicPath(httpRequest) || checkToken(httpRequest)) {
chain.doFilter(request, response);
} else if (checkLogin(httpRequest, httpResponse)) {
continueWithRedirect(httpRequest, httpResponse);
} else if (isRestPath(httpRequest)) {
destroySession(httpRequest);
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
} else {
destroySession(httpRequest);
redirectToIndex(httpRequest, httpResponse);
}
} catch (IllegalArgumentException iae) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
} catch (DAOException e) {
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
@Override
public void init(FilterConfig config) throws ServletException {
}
@Override
public void destroy() {
}
private boolean isLogoutPath(HttpServletRequest request) {
return request.getServletPath().equals(LOGOUT_PATH);
}
private boolean isPublicPath(HttpServletRequest request) {
for (String path : PUBLIC_PATHS) {
if (request.getServletPath().startsWith(path))
return true;
}
return false;
}
private boolean isRestPath(HttpServletRequest request) {
return request.getServletPath().startsWith(REST_PATH);
}
private void redirectToIndex(
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
response.sendRedirect(request.getContextPath());
}
private void continueWithRedirect(
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
String redirectPath = request.getRequestURI();
if (request.getQueryString() != null)
redirectPath += "?" + request.getQueryString();
response.sendRedirect(redirectPath);
}
private void removeTokenCookie(
HttpServletRequest request,
HttpServletResponse response
) {
final Cookie cookie = getTokenCookie(request);
if (cookie != null) {
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
private void destroySession(HttpServletRequest request) {
request.getSession().invalidate();
}
private boolean checkLogin(
HttpServletRequest request,
HttpServletResponse response
) throws DAOException {
final String login = request.getParameter("login");
final String password = request.getParameter("password");
if (login != null && password != null) {
final UsersDAO dao = new UsersDAO();
if (dao.checkLogin(login, password)) {
final Credentials credentials = new Credentials(login, password);
response.addCookie(new Cookie("token", credentials.toToken()));
request.getSession().setAttribute("login", login);
return true;
} else {
return false;
}
} else {
return false;
}
}
private Cookie getTokenCookie(HttpServletRequest request) {
final Cookie[] cookies = Optional.ofNullable(request.getCookies())
.orElse(new Cookie[0]);
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
return cookie;
}
}
return null;
}
private boolean checkToken(HttpServletRequest request)
throws DAOException, IllegalArgumentException {
final Cookie cookie = getTokenCookie(request);
if (cookie != null) {
final Credentials credentials = new Credentials(cookie.getValue());
final UsersDAO dao = new UsersDAO();
if (dao.checkLogin(credentials.getLogin(), credentials.getPassword())) {
request.getSession().setAttribute("login", credentials.getLogin());
return true;
} else {
return false;
}
}
return false;
}
private static class Credentials {
private final String login;
private final String password;
public Credentials(String token) {
final String decodedToken = decodeBase64(token);
final int colonIndex = decodedToken.indexOf(':');
if (colonIndex < 0 || colonIndex == decodedToken.length()-1) {
throw new IllegalArgumentException("Invalid token");
}
this.login = decodedToken.substring(0, colonIndex);
this.password = decodedToken.substring(colonIndex + 1);
}
public Credentials(String login, String password) {
this.login = requireNonNull(login, "Login can't be null");
this.password = requireNonNull(password, "Password can't be null");
}
public String getLogin() {
return login;
}
public String getPassword() {
return password;
}
public String toToken() {
return encodeBase64(this.login + ":" + this.password);
}
private final static String decodeBase64(String text) {
return new String(Base64.getDecoder().decode(text.getBytes()));
}
private final static String encodeBase64(String text) {
return Base64.getEncoder().encodeToString(text.getBytes());
}
}
}
package es.uvigo.esei.daa.dao;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
/**
* Simple base class for DAO (Data Access Object) classes. This super-class is
* responsible for providing a {@link java.sql.Connection} to its sub-classes.
*
* @author Miguel Reboiro Jato
*
*/
public abstract class DAO {
private final static Logger LOG = Logger.getLogger(DAO.class.getName());
private final static String JNDI_NAME = "java:/comp/env/jdbc/daaexample";
private DataSource dataSource;
/**
* Constructs a new instance of {@link DAO}.
*/
public DAO() {
try {
this.dataSource = (DataSource) new InitialContext().lookup(JNDI_NAME);
} catch (NamingException e) {
LOG.log(Level.SEVERE, "Error initializing DAO", e);
throw new RuntimeException(e);
}
}
/**
* Returns an open {@link java.sql.Connection}.
*
* @return an open {@link java.sql.Connection}.
* @throws SQLException if an error happens while establishing the
* connection with the database.
*/
protected Connection getConnection() throws SQLException {
return this.dataSource.getConnection();
}
}
package es.uvigo.esei.daa.dao;
/**
* A general exception class for the DAO layer.
*
* @author Miguel Reboiro Jato
*/
public class DAOException extends Exception {
private static final long serialVersionUID = 1L;
/**
* Constructs a new instance of {@link DAOException} with {@code null} as
* its detail message.
* The cause is not initialized, and may subsequently be initialized by a
* call to {@link #initCause}.
*
*/
public DAOException() {
}
/**
* Constructs a new instance of {@link DAOException} with the specified
* detail message. The cause is not initialized, and may subsequently be
* initialized by a call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for later
* retrieval by the {@link #getMessage()} method.
*/
public DAOException(String message) {
super(message);
}
/**
* Constructs a new instance of {@link DAOException} with the specified
* cause and a detail message of
* {@code (cause==null ? null : cause.toString())} (which typically contains
* the class and detail message of {@code cause}). This constructor is
* useful for exceptions that are little more than wrappers for other
* throwables (for example, {@link java.security.PrivilegedActionException}).
*
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A {@code null} value is permitted, and
* indicates that the cause is nonexistent or unknown.)
*/
public DAOException(Throwable cause) {
super(cause);
}
/**
* Constructs a new instance of {@link DAOException} with the specified
* detail message and cause.
*
* <p>Note that the detail message associated with {@code cause} is
* <i>not</i> automatically incorporated in this exception's detail message.
*
* @param message the detail message (which is saved for later retrieval
* by the {@link #getMessage()} method).
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). A {@code null} value is permitted, and
* indicates that the cause is nonexistent or unknown.
*/
public DAOException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new instance of {@link DAOException} with the specified
* detail message, cause, suppression enabled or disabled, and writable
* stack trace enabled or disabled.
*
* @param message the detail message.
* @param cause the cause. A {@code null} value is permitted, and indicates
* that the cause is nonexistent or unknown.
* @param enableSuppression whether or not suppression is enabled or
* disabled.
* @param writableStackTrace whether or not the stack trace should be
* writable.
*/
public DAOException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
package es.uvigo.esei.daa.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import es.uvigo.esei.daa.entities.Person;
/**
* DAO class for the {@link Person} entities.
*
* @author Miguel Reboiro Jato
*
*/
public class PeopleDAO extends DAO {
private final static Logger LOG = Logger.getLogger(PeopleDAO.class.getName());
/**
* Returns a person stored persisted in the system.
*
* @param id identifier of the person.
* @return a person with the provided identifier.
* @throws DAOException if an error happens while retrieving the person.
* @throws IllegalArgumentException if the provided id does not corresponds
* with any persisted person.
*/
public Person get(int id)
throws DAOException, IllegalArgumentException {
try (final Connection conn = this.getConnection()) {
final String query = "SELECT * FROM people WHERE id=?";
try (final PreparedStatement statement = conn.prepareStatement(query)) {
statement.setInt(1, id);
try (final ResultSet result = statement.executeQuery()) {
if (result.next()) {
return rowToEntity(result);
} else {
throw new IllegalArgumentException("Invalid id");
}
}
}
} catch (SQLException e) {
LOG.log(Level.SEVERE, "Error getting a person", e);
throw new DAOException(e);
}
}
/**
* Returns a list with all the people persisted in the system.
*
* @return a list with all the people persisted in the system.
* @throws DAOException if an error happens while retrieving the people.
*/
public List<Person> list() throws DAOException {
try (final Connection conn = this.getConnection()) {
final String query = "SELECT * FROM people";
try (final PreparedStatement statement = conn.prepareStatement(query)) {
try (final ResultSet result = statement.executeQuery()) {
final List<Person> people = new LinkedList<>();
while (result.next()) {
people.add(rowToEntity(result));
}
return people;
}
}
} catch (SQLException e) {
LOG.log(Level.SEVERE, "Error listing people", e);
throw new DAOException(e);
}
}
/**
* Persists a new person in the system. An identifier will be assigned
* automatically to the new person.
*
* @param name name of the new person. Can't be {@code null}.
* @param surname surname of the new person. Can't be {@code null}.
* @return a {@link Person} entity representing the persisted person.
* @throws DAOException if an error happens while persisting the new person.
* @throws IllegalArgumentException if the name or surname are {@code null}.
*/
public Person add(String name, String surname)
throws DAOException, IllegalArgumentException {
if (name == null || surname == null) {
throw new IllegalArgumentException("name and surname can't be null");
}
try (Connection conn = this.getConnection()) {
final String query = "INSERT INTO people VALUES(null, ?, ?)";
try (PreparedStatement statement = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, name);
statement.setString(2, surname);
if (statement.executeUpdate() == 1) {
try (ResultSet resultKeys = statement.getGeneratedKeys()) {
if (resultKeys.next()) {
return new Person(resultKeys.getInt(1), name, surname);
} else {
LOG.log(Level.SEVERE, "Error retrieving inserted id");
throw new SQLException("Error retrieving inserted id");
}
}
} else {
LOG.log(Level.SEVERE, "Error inserting value");
throw new SQLException("Error inserting value");
}
}
} catch (SQLException e) {
LOG.log(Level.SEVERE, "Error adding a person", e);
throw new DAOException(e);
}
}
/**
* Modifies a person previously persisted in the system. The person will be
* retrieved by the provided id and its current name and surname will be
* replaced with the provided.
*
* @param person a {@link Person} entity with the new data.
* @throws DAOException if an error happens while modifying the new person.
* @throws IllegalArgumentException if the person is {@code null}.
*/
public void modify(Person person)
throws DAOException, IllegalArgumentException {
if (person == null) {
throw new IllegalArgumentException("person can't be null");
}
try (Connection conn = this.getConnection()) {
final String query = "UPDATE people SET name=?, surname=? WHERE id=?";
try (PreparedStatement statement = conn.prepareStatement(query)) {
statement.setString(1, person.getName());
statement.setString(2, person.getSurname());
statement.setInt(3, person.getId());
if (statement.executeUpdate() != 1) {
throw new IllegalArgumentException("name and surname can't be null");
}
}
} catch (SQLException e) {
LOG.log(Level.SEVERE, "Error modifying a person", e);
throw new DAOException();
}
}
/**
* Removes a persisted person from the system.
*
* @param id identifier of the person to be deleted.
* @throws DAOException if an error happens while deleting the person.
* @throws IllegalArgumentException if the provided id does not corresponds
* with any persisted person.
*/
public void delete(int id)
throws DAOException, IllegalArgumentException {
try (final Connection conn = this.getConnection()) {
final String query = "DELETE FROM people WHERE id=?";
try (final PreparedStatement statement = conn.prepareStatement(query)) {
statement.setInt(1, id);
if (statement.executeUpdate() != 1) {
throw new IllegalArgumentException("Invalid id");
}
}
} catch (SQLException e) {
LOG.log(Level.SEVERE, "Error deleting a person", e);
throw new DAOException(e);
}
}
private Person rowToEntity(ResultSet row) throws SQLException {
return new Person(
row.getInt("id"),
row.getString("name"),
row.getString("surname")
);
}
}
package es.uvigo.esei.daa.dao;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
import es.uvigo.esei.daa.entities.User;
/**
* DAO class for managing the users of the system.
*
* @author Miguel Reboiro Jato
*/
public class UsersDAO extends DAO {
private final static Logger LOG = Logger.getLogger(UsersDAO.class.getName());
// Yes, SALT should come from external configuration (for example, a property in Tomcat's context).
private final static String SALT = "daaexample-";
/**
* Returns a user stored persisted in the system.
*
* @param login the login of the user to be retrieved.
* @return a user with the provided login.
* @throws DAOException if an error happens while retrieving the user.
* @throws IllegalArgumentException if the provided login does not
* corresponds with any persisted user.
*/
public User get(String login) throws DAOException {
try (final Connection conn = this.getConnection()) {
final String query = "SELECT * FROM users WHERE login=?";
try (final PreparedStatement statement = conn.prepareStatement(query)) {
statement.setString(1, login);
try (final ResultSet result = statement.executeQuery()) {
if (result.next()) {
return rowToEntity(result);
} else {
throw new IllegalArgumentException("Invalid id");
}
}
}
} catch (SQLException e) {
LOG.log(Level.SEVERE, "Error checking login", e);
throw new DAOException(e);
}
}
/**
* Checks if the provided credentials (login and password) correspond with a
* valid user registered in the system.
*
* <p>The password is stored in the system "salted" and encoded with the
* SHA-256 algorithm.</p>
*
* @param login the login of the user.
* @param password the password of the user.
* @return {@code true} if the credentials are valid. {@code false}
* otherwise.
* @throws DAOException if an error happens while checking the credentials.
*/
public boolean checkLogin(String login, String password) throws DAOException {
try {
final User user = this.get(login);
final String dbPassword = user.getPassword();
final String shaPassword = encodeSha256(SALT + password);
return shaPassword.equals(dbPassword);
} catch (IllegalArgumentException iae) {
return false;
}
}
private final static String encodeSha256(String text) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
final byte[] digested = digest.digest(text.getBytes());
return hexToString(digested);
} catch (NoSuchAlgorithmException e) {
LOG.log(Level.SEVERE, "SHA-256 not supported", e);
throw new RuntimeException(e);
}
}
private final static String hexToString(byte[] hex) {
final StringBuilder sb = new StringBuilder();
for (byte b : hex) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
private User rowToEntity(ResultSet result) throws SQLException {
return new User(
result.getString("login"),
result.getString("password")
);
}
}
package es.uvigo.esei.daa.entities;
import static java.util.Objects.requireNonNull;
/**
* An entity that represents a person.
*
* @author Miguel Reboiro Jato
*/
public class Person {
private int id;
private String name;
private String surname;
// Constructor needed for the JSON conversion
Person() {}
/**
* Constructs a new instance of {@link Person}.
*
* @param id identifier of the person.
* @param name name of the person.
* @param surname surname of the person.
*/
public Person(int id, String name, String surname) {
this.id = id;
this.setName(name);
this.setSurname(surname);
}
/**
* Returns the identifier of the person.
*
* @return the identifier of the person.
*/
public int getId() {
return id;
}
/**
* Returns the name of the person.
*
* @return the name of the person.
*/
public String getName() {
return name;
}
/**
* Set the name of this person.
*
* @param name the new name of the person.
* @throws NullPointerException if the {@code name} is {@code null}.
*/
public void setName(String name) {
this.name = requireNonNull(name, "Name can't be null");
}
/**
* Returns the surname of the person.
*
* @return the surname of the person.
*/
public String getSurname() {
return surname;
}
/**
* Set the surname of this person.
*
* @param surname the new surname of the person.
* @throws NullPointerException if the {@code surname} is {@code null}.
*/
public void setSurname(String surname) {
this.surname = requireNonNull(surname, "Surname can't be null");
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Person))
return false;
Person other = (Person) obj;
if (id != other.id)
return false;
return true;
}
}
package es.uvigo.esei.daa.entities;
import static java.util.Objects.requireNonNull;
/**
* An entity that represents a user.
*
* @author Miguel Reboiro Jato
*/
public class User {
private String login;
private String password;
// Constructor needed for the JSON conversion
User() {}
/**
* Constructs a new instance of {@link User}.
*
* @param login login that identifies the user in the system.
* @param password password of the user encoded using SHA-256 and with the
* "salt" prefix added.
*/
public User(String login, String password) {
this.setLogin(login);
this.setPassword(password);
}
/**
* Returns the login of the user.
*
* @return the login of the user.
*/
public String getLogin() {
return login;
}
/**
* Sets the login of the user.
*
* @param login the login that identifies the user in the system.
*/
public void setLogin(String login) {
this.login = requireNonNull(login, "Login can't be null");
}
/**
* Returns the password of the user.
*
* @return the password of the user.
*/
public String getPassword() {
return password;
}
/**
* Sets the users password.
* @param password the password of the user encoded using SHA-256 and with
* the "salt" prefix added.
*/
public void setPassword(String password) {
requireNonNull(password, "Password can't be null");
if (!password.matches("[a-zA-Z0-9]{64}"))
throw new IllegalArgumentException("Password must be a valid SHA-256");
this.password = password;
}
}
package es.uvigo.esei.daa.rest;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import es.uvigo.esei.daa.dao.DAOException;
import es.uvigo.esei.daa.dao.PeopleDAO;
import es.uvigo.esei.daa.entities.Person;
/**
* REST resource for managing people.
*
* @author Miguel Reboiro Jato.
*/
@Path("/people")
@Produces(MediaType.APPLICATION_JSON)
public class PeopleResource {
private final static Logger LOG = Logger.getLogger(PeopleResource.class.getName());
private final PeopleDAO dao;
/**
* Constructs a new instance of {@link PeopleResource}.
*/
public PeopleResource() {
this(new PeopleDAO());
}
// Needed for testing purposes
PeopleResource(PeopleDAO dao) {
this.dao = dao;
}
/**
* Returns a person with the provided identifier.
*
* @param id the identifier of the person to retrieve.
* @return a 200 OK response with a person that has the provided identifier.
* If the identifier does not corresponds with any user, a 400 Bad Request
* response with an error message will be returned. If an error happens
* while retrieving the list, a 500 Internal Server Error response with an
* error message will be returned.
*/
@GET
@Path("/{id}")
public Response get(
@PathParam("id") int id
) {
try {
final Person person = this.dao.get(id);
return Response.ok(person).build();
} catch (IllegalArgumentException iae) {
LOG.log(Level.FINE, "Invalid person id in get method", iae);
return Response.status(Response.Status.BAD_REQUEST)
.entity(iae.getMessage())
.build();
} catch (DAOException e) {
LOG.log(Level.SEVERE, "Error getting a person", e);
return Response.serverError()
.entity(e.getMessage())
.build();
}
}
/**
* Returns the complete list of people stored in the system.
*
* @return a 200 OK response with the complete list of people stored in the
* system. If an error happens while retrieving the list, a 500 Internal
* Server Error response with an error message will be returned.
*/
@GET
public Response list() {
try {
return Response.ok(this.dao.list()).build();
} catch (DAOException e) {
LOG.log(Level.SEVERE, "Error listing people", e);
return Response.serverError().entity(e.getMessage()).build();
}
}
/**
* Creates a new person in the system.
*
* @param name the name of the new person.
* @param surname the surname of the new person.
* @return a 200 OK response with a person that has been created. If the
* name or the surname are not provided, a 400 Bad Request response with an
* error message will be returned. If an error happens while retrieving the
* list, a 500 Internal Server Error response with an error message will be
* returned.
*/
@POST
public Response add(
@FormParam("name") String name,
@FormParam("surname") String surname
) {
try {
final Person newPerson = this.dao.add(name, surname);
return Response.ok(newPerson).build();
} catch (IllegalArgumentException iae) {
LOG.log(Level.FINE, "Invalid person id in add method", iae);
return Response.status(Response.Status.BAD_REQUEST)
.entity(iae.getMessage())
.build();
} catch (DAOException e) {
LOG.log(Level.SEVERE, "Error adding a person", e);
return Response.serverError()
.entity(e.getMessage())
.build();
}
}
/**
* Modifies the data of a person.
*
* @param id identifier of the person to modify.
* @param name the new name of the person.
* @param surname the new surname of the person.
* @return a 200 OK response with a person that has been modified. If the
* identifier does not corresponds with any user or the name or surname are
* not provided, a 400 Bad Request response with an error message will be
* returned. If an error happens while retrieving the list, a 500 Internal
* Server Error response with an error message will be returned.
*/
@PUT
@Path("/{id}")
public Response modify(
@PathParam("id") int id,
@FormParam("name") String name,
@FormParam("surname") String surname
) {
try {
final Person modifiedPerson = new Person(id, name, surname);
this.dao.modify(modifiedPerson);
return Response.ok(modifiedPerson).build();
} catch (NullPointerException npe) {
final String message = String.format("Invalid data for person (name: %s, surname: %s)", name, surname);
LOG.log(Level.FINE, message);
return Response.status(Response.Status.BAD_REQUEST)
.entity(message)
.build();
} catch (IllegalArgumentException iae) {
LOG.log(Level.FINE, "Invalid person id in modify method", iae);
return Response.status(Response.Status.BAD_REQUEST)
.entity(iae.getMessage())
.build();
} catch (DAOException e) {
LOG.log(Level.SEVERE, "Error modifying a person", e);
return Response.serverError()
.entity(e.getMessage())
.build();
}
}
/**
* Deletes a person from the system.
*
* @param id the identifier of the person to be deleted.
* @return a 200 OK response with the identifier of the person that has
* been deleted. If the identifier does not corresponds with any user, a 400
* Bad Request response with an error message will be returned. If an error
* happens while retrieving the list, a 500 Internal Server Error response
* with an error message will be returned.
*/
@DELETE
@Path("/{id}")
public Response delete(
@PathParam("id") int id
) {
try {
this.dao.delete(id);
return Response.ok(id).build();
} catch (IllegalArgumentException iae) {
LOG.log(Level.FINE, "Invalid person id in delete method", iae);
return Response.status(Response.Status.BAD_REQUEST)
.entity(iae.getMessage())
.build();
} catch (DAOException e) {
LOG.log(Level.SEVERE, "Error deleting a person", e);
return Response.serverError()
.entity(e.getMessage())
.build();
}
}
}
package es.uvigo.esei.daa.rest;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import es.uvigo.esei.daa.dao.DAOException;
import es.uvigo.esei.daa.dao.UsersDAO;
/**
* REST resource for managing users.
*
* @author Miguel Reboiro Jato.
*/
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
public class UsersResource {
private final static Logger LOG = Logger.getLogger(UsersResource.class.getName());
private final UsersDAO dao;
// The HttpServletRequest can be also injected as a parameter in the REST
// methods.
private @Context HttpServletRequest request;
/**
* Constructs a new instance of {@link UsersResource}.
*/
public UsersResource() {
this(new UsersDAO());
}
// Needed for testing purposes
UsersResource(UsersDAO dao) {
this(dao, null);
}
// Needed for testing purposes
UsersResource(UsersDAO dao, HttpServletRequest request) {
this.dao = dao;
this.request = request;
}
/**
* Returns a user with the provided login.
*
* @param login the identifier of the user to retrieve.
* @return a 200 OK response with an user that has the provided login.
* If the request is done without providing the login credentials or using
* invalid credentials a 401 Unauthorized response will be returned. If the
* credentials are provided and a regular user (i.e. non admin user) tries
* to access the data of other user, a 403 Forbidden response will be
* returned. If the credentials are OK, but the login does not corresponds
* with any user, a 400 Bad Request response with an error message will be
* returned. If an error happens while retrieving the list, a 500 Internal
* Server Error response with an error message will be returned.
*/
@GET
@Path("/{login}")
public Response get(
@PathParam("login") String login
) {
final String loggedUser = getLogin();
// Each user can only access his or her own data. Only the admin user
// can access the data of any user.
if (loggedUser.equals(login) || loggedUser.equals("admin")) {
try {
return Response.ok(dao.get(login)).build();
} catch (IllegalArgumentException iae) {
LOG.log(Level.FINE, "Invalid user login in get method", iae);
return Response.status(Response.Status.BAD_REQUEST)
.entity(iae.getMessage())
.build();
} catch (DAOException e) {
LOG.log(Level.SEVERE, "Error getting an user", e);
return Response.serverError()
.entity(e.getMessage())
.build();
}
} else {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
private String getLogin() {
return (String) request.getSession().getAttribute("login");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>DAAExample</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
\ No newline at end of file
html, body {
height: 100%;
}
body {
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-align: center;
-ms-flex-pack: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
#login {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
#password {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>DAA Example - Login</title>
<link rel="stylesheet" href="css/login.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body class="text-center">
<form class="form-singin" action="main.html" method="POST">
<h1 class="h1 mb-3 font-weight-normal">DAA Example</h1>
<label for="login" class="sr-only">Usuario</label>
<input id="login" name="login" type="text" class="form-control" placeholder="Usuario" required autofocus />
<label for="password" class="sr-only">Contraseña</label>
<input id="password" name="password" type="password" class="form-control" placeholder="Contraseña" required />
<button type="submit" class="btn btn-lg btn-primary btn-block mt-3">Entrar</button>
</form>
</body>
</html>
\ No newline at end of file
var PeopleDAO = (function() {
var resourcePath = "rest/people/";
var requestByAjax = function(data, done, fail, always) {
done = typeof done !== 'undefined' ? done : function() {};
fail = typeof fail !== 'undefined' ? fail : function() {};
always = typeof always !== 'undefined' ? always : function() {};
$.ajax(data)
.done(done)
.fail(fail)
.always(always);
};
function PeopleDAO() {
this.listPeople = function(done, fail, always) {
requestByAjax({
url: resourcePath,
type: 'GET'
}, done, fail, always);
};
this.addPerson = function(person, done, fail, always) {
requestByAjax({
url: resourcePath,
type: 'POST',
data: person
}, done, fail, always);
};
this.modifyPerson = function(person, done, fail, always) {
requestByAjax({
url: resourcePath + person.id,
type: 'PUT',
data: person
}, done, fail, always);
};
this.deletePerson = function(id, done, fail, always) {
requestByAjax({
url: resourcePath + id,
type: 'DELETE',
}, done, fail, always);
};
}
return PeopleDAO;
})();
\ No newline at end of file
var PeopleView = (function() {
var dao;
// Referencia a this que permite acceder a las funciones públicas desde las funciones de jQuery.
var self;
var formId = 'people-form';
var listId = 'people-list';
var formQuery = '#' + formId;
var listQuery = '#' + listId;
function PeopleView(peopleDao, formContainerId, listContainerId) {
dao = peopleDao;
self = this;
insertPeopleForm($('#' + formContainerId));
insertPeopleList($('#' + listContainerId));
this.init = function() {
dao.listPeople(function(people) {
$.each(people, function(key, person) {
appendToTable(person);
});
});
// La acción por defecto de enviar formulario (submit) se sobreescribe
// para que el envío sea a través de AJAX
$(formQuery).submit(function(event) {
var person = self.getPersonInForm();
if (self.isEditing()) {
dao.modifyPerson(person,
function(person) {
$('#person-' + person.id + ' td.name').text(person.name);
$('#person-' + person.id + ' td.surname').text(person.surname);
self.resetForm();
},
showErrorMessage,
self.enableForm
);
} else {
dao.addPerson(person,
function(person) {
appendToTable(person);
self.resetForm();
},
showErrorMessage,
self.enableForm
);
}
return false;
});
$('#btnClear').click(this.resetForm);
};
this.getPersonInForm = function() {
var form = $(formQuery);
return {
'id': form.find('input[name="id"]').val(),
'name': form.find('input[name="name"]').val(),
'surname': form.find('input[name="surname"]').val()
};
};
this.getPersonInRow = function(id) {
var row = $('#person-' + id);
if (row !== undefined) {
return {
'id': id,
'name': row.find('td.name').text(),
'surname': row.find('td.surname').text()
};
} else {
return undefined;
}
};
this.editPerson = function(id) {
var row = $('#person-' + id);
if (row !== undefined) {
var form = $(formQuery);
form.find('input[name="id"]').val(id);
form.find('input[name="name"]').val(row.find('td.name').text());
form.find('input[name="surname"]').val(row.find('td.surname').text());
$('input#btnSubmit').val('Modificar');
}
};
this.deletePerson = function(id) {
if (confirm('Está a punto de eliminar a una persona. ¿Está seguro de que desea continuar?')) {
dao.deletePerson(id,
function() {
$('tr#person-' + id).remove();
},
showErrorMessage
);
}
};
this.isEditing = function() {
return $(formQuery + ' input[name="id"]').val() != "";
};
this.disableForm = function() {
$(formQuery + ' input').prop('disabled', true);
};
this.enableForm = function() {
$(formQuery + ' input').prop('disabled', false);
};
this.resetForm = function() {
$(formQuery)[0].reset();
$(formQuery + ' input[name="id"]').val('');
$('#btnSubmit').val('Crear');
};
};
var insertPeopleList = function(parent) {
parent.append(
'<table id="' + listId + '" class="table">\
<thead>\
<tr class="row">\
<th class="col-sm-4">Nombre</th>\
<th class="col-sm-5">Apellido</th>\
<th class="col-sm-3">&nbsp;</th>\
</tr>\
</thead>\
<tbody>\
</tbody>\
</table>'
);
};
var insertPeopleForm = function(parent) {
parent.append(
'<form id="' + formId + '" class="mb-5 mb-10">\
<input name="id" type="hidden" value=""/>\
<div class="row">\
<div class="col-sm-4">\
<input name="name" type="text" value="" placeholder="Nombre" class="form-control" required/>\
</div>\
<div class="col-sm-5">\
<input name="surname" type="text" value="" placeholder="Apellido" class="form-control" required/>\
</div>\
<div class="col-sm-3">\
<input id="btnSubmit" type="submit" value="Crear" class="btn btn-primary" />\
<input id="btnClear" type="reset" value="Limpiar" class="btn" />\
</div>\
</div>\
</form>'
);
};
var createPersonRow = function(person) {
return '<tr id="person-'+ person.id +'" class="row">\
<td class="name col-sm-4">' + person.name + '</td>\
<td class="surname col-sm-5">' + person.surname + '</td>\
<td class="col-sm-3">\
<a class="edit btn btn-primary" href="#">Editar</a>\
<a class="delete btn btn-warning" href="#">Eliminar</a>\
</td>\
</tr>';
};
var showErrorMessage = function(jqxhr, textStatus, error) {
alert(textStatus + ": " + error);
};
var addRowListeners = function(person) {
$('#person-' + person.id + ' a.edit').click(function() {
self.editPerson(person.id);
});
$('#person-' + person.id + ' a.delete').click(function() {
self.deletePerson(person.id);
});
};
var appendToTable = function(person) {
$(listQuery + ' > tbody:last')
.append(createPersonRow(person));
addRowListeners(person);
};
return PeopleView;
})();
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>DAA Example</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark box-shadow">
<div class="container d-flex justify-content-between">
<a href="#" class="navbar-brand d-flex align-items-center">
<strong>DAA Example</strong>
</a>
<a id="#logout" href="logout" class="btn btn-dark" role="button">Cerrar sesión</a>
</div>
</div>
</header>
<div class="container">
<div id="people-container">
<h1 class="display-5 mt-3 mb-3">Personas</h1>
</div>
</div>
<script type="text/javascript" src="http://code.jquery.com/jquery-2.2.4.min.js"></script>
<script type="text/javascript" src="js/dao/people.js"></script>
<script type="text/javascript" src="js/view/people.js"></script>
<script type="text/javascript">
$(document).ready(function() {
var view = new PeopleView(new PeopleDAO(), 'people-container', 'people-container');
view.init();
});
</script>
</body>
</html>
\ No newline at end of file
package es.uvigo.esei.daa.dao;
import static es.uvigo.esei.daa.dataset.PeopleDataset.existentId;
import static es.uvigo.esei.daa.dataset.PeopleDataset.existentPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newName;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newSurname;
import static es.uvigo.esei.daa.dataset.PeopleDataset.nonExistentId;
import static es.uvigo.esei.daa.dataset.PeopleDataset.nonExistentPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.people;
import static es.uvigo.esei.daa.dataset.PeopleDataset.peopleWithout;
import static es.uvigo.esei.daa.matchers.IsEqualToPerson.containsPeopleInAnyOrder;
import static es.uvigo.esei.daa.matchers.IsEqualToPerson.equalsToPerson;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import es.uvigo.esei.daa.entities.Person;
import es.uvigo.esei.daa.listeners.ApplicationContextBinding;
import es.uvigo.esei.daa.listeners.ApplicationContextJndiBindingTestExecutionListener;
import es.uvigo.esei.daa.listeners.DbManagement;
import es.uvigo.esei.daa.listeners.DbManagementTestExecutionListener;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:contexts/mem-context.xml")
@TestExecutionListeners({
DbUnitTestExecutionListener.class,
DbManagementTestExecutionListener.class,
ApplicationContextJndiBindingTestExecutionListener.class
})
@ApplicationContextBinding(
jndiUrl = "java:/comp/env/jdbc/daaexample",
type = DataSource.class
)
@DbManagement(
create = "classpath:db/hsqldb.sql",
drop = "classpath:db/hsqldb-drop.sql"
)
@DatabaseSetup("/datasets/dataset.xml")
@ExpectedDatabase("/datasets/dataset.xml")
public class PeopleDAOTest {
private PeopleDAO dao;
@Before
public void setUp() throws Exception {
this.dao = new PeopleDAO();
}
@Test
public void testList() throws DAOException {
assertThat(this.dao.list(), containsPeopleInAnyOrder(people()));
}
@Test
public void testGet() throws DAOException {
final Person person = this.dao.get(existentId());
assertThat(person, is(equalsToPerson(existentPerson())));
}
@Test(expected = IllegalArgumentException.class)
public void testGetNonExistentId() throws DAOException {
this.dao.get(nonExistentId());
}
@Test
@ExpectedDatabase("/datasets/dataset-delete.xml")
public void testDelete() throws DAOException {
this.dao.delete(existentId());
assertThat(this.dao.list(), containsPeopleInAnyOrder(peopleWithout(existentId())));
}
@Test(expected = IllegalArgumentException.class)
public void testDeleteNonExistentId() throws DAOException {
this.dao.delete(nonExistentId());
}
@Test
@ExpectedDatabase("/datasets/dataset-modify.xml")
public void testModify() throws DAOException {
final Person person = existentPerson();
person.setName(newName());
person.setSurname(newSurname());
this.dao.modify(person);
final Person persistentPerson = this.dao.get(person.getId());
assertThat(persistentPerson, is(equalsToPerson(person)));
}
@Test(expected = IllegalArgumentException.class)
public void testModifyNonExistentId() throws DAOException {
this.dao.modify(nonExistentPerson());
}
@Test(expected = IllegalArgumentException.class)
public void testModifyNullPerson() throws DAOException {
this.dao.modify(null);
}
@Test
@ExpectedDatabase("/datasets/dataset-add.xml")
public void testAdd() throws DAOException {
final Person person = this.dao.add(newName(), newSurname());
assertThat(person, is(equalsToPerson(newPerson())));
final Person persistentPerson = this.dao.get(person.getId());
assertThat(persistentPerson, is(equalsToPerson(newPerson())));
}
@Test(expected = IllegalArgumentException.class)
public void testAddNullName() throws DAOException {
this.dao.add(null, newSurname());
}
@Test(expected = IllegalArgumentException.class)
public void testAddNullSurname() throws DAOException {
this.dao.add(newName(), null);
}
}
package es.uvigo.esei.daa.dao;
import static es.uvigo.esei.daa.dataset.PeopleDataset.existentId;
import static es.uvigo.esei.daa.dataset.PeopleDataset.existentPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newName;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newSurname;
import static es.uvigo.esei.daa.dataset.PeopleDataset.people;
import static es.uvigo.esei.daa.matchers.IsEqualToPerson.containsPeopleInAnyOrder;
import static es.uvigo.esei.daa.matchers.IsEqualToPerson.equalsToPerson;
import static org.easymock.EasyMock.anyString;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.reset;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.sql.SQLException;
import org.junit.Test;
import com.mysql.jdbc.Statement;
import es.uvigo.esei.daa.entities.Person;
import es.uvigo.esei.daa.util.DatabaseQueryUnitTest;
public class PeopleDAOUnitTest extends DatabaseQueryUnitTest {
@Test
public void testList() throws Exception {
final Person[] people = people();
for (Person person : people) {
expectPersonRow(person);
}
expect(result.next()).andReturn(false);
result.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
assertThat(peopleDAO.list(), containsPeopleInAnyOrder(people));
}
@Test(expected = DAOException.class)
public void testListUnexpectedException() throws Exception {
expect(result.next()).andThrow(new SQLException());
result.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.list();
}
@Test
public void testGet() throws Exception {
final Person existentPerson = existentPerson();
expectPersonRow(existentPerson);
result.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
assertThat(peopleDAO.get(existentId()), is(equalTo(existentPerson)));
}
@Test(expected = IllegalArgumentException.class)
public void testGetMissing() throws Exception {
expect(result.next()).andReturn(false);
result.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.get(existentId());
}
@Test(expected = DAOException.class)
public void testGetUnexpectedException() throws Exception {
expect(result.next()).andThrow(new SQLException());
result.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.get(existentId());
}
@Test
public void testAdd() throws Exception {
final Person person = newPerson();
reset(connection);
expect(connection.prepareStatement(anyString(), eq(Statement.RETURN_GENERATED_KEYS)))
.andReturn(statement);
expect(statement.executeUpdate()).andReturn(1);
expect(statement.getGeneratedKeys()).andReturn(result);
// Key retrieval
expect(result.next()).andReturn(true);
expect(result.getInt(1)).andReturn(person.getId());
connection.close();
result.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
final Person newPerson = peopleDAO.add(person.getName(), person.getSurname());
assertThat(newPerson, is(equalsToPerson(person)));
}
@Test(expected = IllegalArgumentException.class)
public void testAddNullName() throws Exception {
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
resetAll(); // No expectations
peopleDAO.add(null, newSurname());
}
@Test(expected = IllegalArgumentException.class)
public void testAddNullSurname() throws Exception {
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
resetAll(); // No expectations
peopleDAO.add(newName(), null);
}
@Test(expected = DAOException.class)
public void testAddZeroUpdatedRows() throws Exception {
reset(connection);
expect(connection.prepareStatement(anyString(), eq(1)))
.andReturn(statement);
expect(statement.executeUpdate()).andReturn(0);
connection.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.add(newName(), newSurname());
}
@Test(expected = DAOException.class)
public void testAddNoGeneratedKey() throws Exception {
reset(connection);
expect(connection.prepareStatement(anyString(), eq(1)))
.andReturn(statement);
expect(statement.executeUpdate()).andReturn(1);
expect(statement.getGeneratedKeys()).andReturn(result);
expect(result.next()).andReturn(false);
result.close();
connection.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.add(newName(), newSurname());
}
@Test(expected = DAOException.class)
public void testAddUnexpectedException() throws Exception {
reset(connection);
expect(connection.prepareStatement(anyString(), eq(1)))
.andReturn(statement);
expect(statement.executeUpdate()).andThrow(new SQLException());
connection.close();
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.add(newName(), newSurname());
}
@Test
public void testDelete() throws Exception {
expect(statement.executeUpdate()).andReturn(1);
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.delete(existentId());
}
@Test(expected = IllegalArgumentException.class)
public void testDeleteInvalidId() throws Exception {
expect(statement.executeUpdate()).andReturn(0);
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.delete(existentId());
}
@Test(expected = DAOException.class)
public void testDeleteUnexpectedException() throws Exception {
expect(statement.executeUpdate()).andThrow(new SQLException());
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.delete(existentId());
}
@Test
public void testModify() throws Exception {
expect(statement.executeUpdate()).andReturn(1);
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.modify(existentPerson());
}
@Test(expected = IllegalArgumentException.class)
public void testModifyNullPerson() throws Exception {
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
resetAll(); // No expectations
peopleDAO.modify(null);
}
@Test(expected = IllegalArgumentException.class)
public void testModifyZeroUpdatedRows() throws Exception {
expect(statement.executeUpdate()).andReturn(0);
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.modify(existentPerson());
}
@Test(expected = DAOException.class)
public void testModifyUnexpectedException() throws Exception {
expect(statement.executeUpdate()).andThrow(new SQLException());
replayAll();
final PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.modify(existentPerson());
}
private void expectPersonRow(Person person) throws SQLException {
expect(result.next()).andReturn(true);
expect(result.getInt("id")).andReturn(person.getId());
expect(result.getString("name")).andReturn(person.getName());
expect(result.getString("surname")).andReturn(person.getSurname());
}
}
package es.uvigo.esei.daa.dataset;
import static java.util.Arrays.binarySearch;
import static java.util.Arrays.stream;
import java.util.Arrays;
import java.util.function.Predicate;
import es.uvigo.esei.daa.entities.Person;
public final class PeopleDataset {
private PeopleDataset() {}
public static Person[] people() {
return new Person[] {
new Person(1, "Antón", "Álvarez"),
new Person(2, "Ana", "Amargo"),
new Person(3, "Manuel", "Martínez"),
new Person(4, "María", "Márquez"),
new Person(5, "Lorenzo", "López"),
new Person(6, "Laura", "Laredo"),
new Person(7, "Perico", "Palotes"),
new Person(8, "Patricia", "Pérez"),
new Person(9, "Julia", "Justa"),
new Person(10, "Juan", "Jiménez")
};
}
public static Person[] peopleWithout(int ... ids) {
Arrays.sort(ids);
final Predicate<Person> hasValidId = person ->
binarySearch(ids, person.getId()) < 0;
return stream(people())
.filter(hasValidId)
.toArray(Person[]::new);
}
public static Person person(int id) {
return stream(people())
.filter(person -> person.getId() == id)
.findAny()
.orElseThrow(IllegalArgumentException::new);
}
public static int existentId() {
return 5;
}
public static int nonExistentId() {
return 1234;
}
public static Person existentPerson() {
return person(existentId());
}
public static Person nonExistentPerson() {
return new Person(nonExistentId(), "Jane", "Smith");
}
public static String newName() {
return "John";
}
public static String newSurname() {
return "Doe";
}
public static Person newPerson() {
return new Person(people().length + 1, newName(), newSurname());
}
}
package es.uvigo.esei.daa.dataset;
import java.util.Arrays;
import java.util.Base64;
import es.uvigo.esei.daa.entities.User;
public final class UsersDataset {
private UsersDataset() {}
public static User[] users() {
return new User[] {
new User(adminLogin(), "43f413b773f7d0cfad0e8e6529ec1249ce71e8697919eab30d82d800a3986b70"),
new User(normalLogin(), "688f21dd2d65970f174e2c9d35159250a8a23e27585452683db8c5d10b586336")
};
}
public static User user(String login) {
return Arrays.stream(users())
.filter(user -> user.getLogin().equals(login))
.findAny()
.orElseThrow(IllegalArgumentException::new);
}
public static String adminLogin() {
return "admin";
}
public static String normalLogin() {
return "normal";
}
public static String userToken(String login) {
final String chain = login + ":" + login + "pass";
return Base64.getEncoder().encodeToString(chain.getBytes());
}
}
package es.uvigo.esei.daa.entities;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import nl.jqno.equalsverifier.EqualsVerifier;
import nl.jqno.equalsverifier.Warning;
public class PersonUnitTest {
@Test
public void testPersonIntStringString() {
final int id = 1;
final String name = "John";
final String surname = "Doe";
final Person person = new Person(id, name, surname);
assertThat(person.getId(), is(equalTo(id)));
assertThat(person.getName(), is(equalTo(name)));
assertThat(person.getSurname(), is(equalTo(surname)));
}
@Test(expected = NullPointerException.class)
public void testPersonIntStringStringNullName() {
new Person(1, null, "Doe");
}
@Test(expected = NullPointerException.class)
public void testPersonIntStringStringNullSurname() {
new Person(1, "John", null);
}
@Test
public void testSetName() {
final int id = 1;
final String surname = "Doe";
final Person person = new Person(id, "John", surname);
person.setName("Juan");
assertThat(person.getId(), is(equalTo(id)));
assertThat(person.getName(), is(equalTo("Juan")));
assertThat(person.getSurname(), is(equalTo(surname)));
}
@Test(expected = NullPointerException.class)
public void testSetNullName() {
final Person person = new Person(1, "John", "Doe");
person.setName(null);
}
@Test
public void testSetSurname() {
final int id = 1;
final String name = "John";
final Person person = new Person(id, name, "Doe");
person.setSurname("Dolores");
assertThat(person.getId(), is(equalTo(id)));
assertThat(person.getName(), is(equalTo(name)));
assertThat(person.getSurname(), is(equalTo("Dolores")));
}
@Test(expected = NullPointerException.class)
public void testSetNullSurname() {
final Person person = new Person(1, "John", "Doe");
person.setSurname(null);
}
@Test
public void testEqualsObject() {
final Person personA = new Person(1, "Name A", "Surname A");
final Person personB = new Person(1, "Name B", "Surname B");
assertTrue(personA.equals(personB));
}
@Test
public void testEqualsHashcode() {
EqualsVerifier.forClass(Person.class)
.withIgnoredFields("name", "surname")
.suppress(Warning.STRICT_INHERITANCE)
.suppress(Warning.NONFINAL_FIELDS)
.verify();
}
}
package es.uvigo.esei.daa.listeners;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ApplicationContextBindings.class)
public @interface ApplicationContextBinding {
public String jndiUrl();
public String name() default "";
public Class<?> type() default None.class;
public final static class None {}
}
package es.uvigo.esei.daa.listeners;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationContextBindings {
public ApplicationContextBinding[] value();
}
package es.uvigo.esei.daa.listeners;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import es.uvigo.esei.daa.listeners.ApplicationContextBinding.None;
public class ApplicationContextJndiBindingTestExecutionListener extends AbstractTestExecutionListener {
private SimpleNamingContextBuilder contextBuilder;
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
final Class<?> testClass = testContext.getTestClass();
final ApplicationContextBinding[] bindings = testClass.getAnnotationsByType(ApplicationContextBinding.class);
this.contextBuilder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
for (ApplicationContextBinding binding : bindings) {
final String bindingName = binding.name();
final Class<?> bindingType = binding.type();
Object bean;
if (bindingName.isEmpty() && bindingType.equals(None.class)) {
throw new IllegalArgumentException("name or type attributes must be configured in ApplicationContextBinding");
} else if (bindingName.isEmpty()) {
bean = testContext.getApplicationContext().getBean(bindingType);
} else if (bindingType.equals(None.class)) {
bean = testContext.getApplicationContext().getBean(bindingName);
} else {
bean = testContext.getApplicationContext().getBean(bindingName, bindingType);
}
this.contextBuilder.bind(binding.jndiUrl(), bean);
}
}
@Override
public void afterTestClass(TestContext testContext) throws Exception {
this.contextBuilder.clear();
this.contextBuilder = null;
}
}
package es.uvigo.esei.daa.listeners;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbManagement {
public String[] create() default "";
public String[] drop() default "";
public DbManagementAction action() default DbManagementAction.CREATE_DROP;
}
package es.uvigo.esei.daa.listeners;
public enum DbManagementAction {
DROP_CREATE_DROP, CREATE_DROP, ONLY_CREATE, ONLY_DROP;
}
package es.uvigo.esei.daa.listeners;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
public class DbManagementTestExecutionListener extends AbstractTestExecutionListener {
private DbManagement configuration;
private DataSource datasource;
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
final Class<?> testClass = testContext.getTestClass();
this.configuration = testClass.getAnnotation(DbManagement.class);
if (this.configuration == null)
throw new IllegalStateException(String.format(
"Missing %s annotation in %s class",
DbManagement.class.getSimpleName(), testClass.getName()
));
this.datasource = testContext.getApplicationContext().getBean(DataSource.class);
switch (this.configuration.action()) {
case DROP_CREATE_DROP:
executeDrop();
case CREATE_DROP:
case ONLY_CREATE:
executeCreate();
break;
default:
}
}
@Override
public void afterTestClass(TestContext testContext) throws Exception {
try {
switch (this.configuration.action()) {
case DROP_CREATE_DROP:
case CREATE_DROP:
case ONLY_DROP:
executeDrop();
break;
default:
}
} finally {
this.configuration = null;
this.datasource = null;
}
}
private void executeCreate() throws SQLException, IOException {
this.executeQueries(configuration.create());
}
private void executeDrop() throws SQLException, IOException {
this.executeQueries(configuration.drop());
}
private void executeQueries(String ... queriesPaths)
throws SQLException, IOException {
try (Connection connection = this.datasource.getConnection()) {
try (Statement statement = connection.createStatement()) {
for (String queryPath : queriesPaths) {
final String queries = readFile(queryPath);
for (String query : queries.split(";")) {
query = query.trim();
if (!query.trim().isEmpty()) {
statement.addBatch(query);
}
}
}
statement.executeBatch();
}
}
}
private static String readFile(String path) throws IOException {
final String classpathPrefix = "classpath:";
if (path.startsWith(classpathPrefix)) {
path = path.substring(classpathPrefix.length());
final ClassLoader classLoader =
DbManagementTestExecutionListener.class.getClassLoader();
final InputStream fileIS = classLoader.getResourceAsStream(path);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(fileIS))) {
final StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
} else {
return new String(Files.readAllBytes(Paths.get(path)));
}
}
}
/*
* #%L
* PanDrugsDB Backend
* %%
* Copyright (C) 2015 Fátima Al-Shahrour, Elena Piñeiro, Daniel Glez-Peña and Miguel Reboiro-Jato
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
package es.uvigo.esei.daa.matchers;
import static java.util.Objects.requireNonNull;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.StatusType;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
public class HasHttpStatus extends TypeSafeMatcher<Response> {
private final StatusType expectedStatus;
public HasHttpStatus(StatusType expectedStatus) {
this.expectedStatus = requireNonNull(expectedStatus);
}
public HasHttpStatus(int expectedStatus) {
this(Status.fromStatusCode(expectedStatus));
}
@Override
public void describeTo(Description description) {
description.appendValue(this.expectedStatus);
}
@Override
protected boolean matchesSafely(Response item) {
return item != null && expectedStatus.getStatusCode() == item.getStatusInfo().getStatusCode();
}
@Factory
public static Matcher<Response> hasHttpStatus(StatusType expectedStatus) {
return new HasHttpStatus(expectedStatus);
}
@Factory
public static Matcher<Response> hasHttpStatus(int expectedStatus) {
return new HasHttpStatus(expectedStatus);
}
@Factory
public static Matcher<Response> hasOkStatus() {
return new HasHttpStatus(Response.Status.OK);
}
@Factory
public static Matcher<Response> hasBadRequestStatus() {
return new HasHttpStatus(Response.Status.BAD_REQUEST);
}
@Factory
public static Matcher<Response> hasInternalServerErrorStatus() {
return new HasHttpStatus(Response.Status.INTERNAL_SERVER_ERROR);
}
@Factory
public static Matcher<Response> hasUnauthorized() {
return new HasHttpStatus(Response.Status.UNAUTHORIZED);
}
@Factory
public static Matcher<Response> hasForbidden() {
return new HasHttpStatus(Response.Status.FORBIDDEN);
}
}
This diff is collapsed.
package es.uvigo.esei.daa.matchers;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import es.uvigo.esei.daa.entities.Person;
public class IsEqualToPerson extends IsEqualToEntity<Person> {
public IsEqualToPerson(Person entity) {
super(entity);
}
@Override
protected boolean matchesSafely(Person actual) {
this.clearDescribeTo();
if (actual == null) {
this.addTemplatedDescription("actual", expected.toString());
return false;
} else {
return checkAttribute("id", Person::getId, actual)
&& checkAttribute("name", Person::getName, actual)
&& checkAttribute("surname", Person::getSurname, actual);
}
}
/**
* Factory method that creates a new {@link IsEqualToEntity} matcher with
* the provided {@link Person} as the expected value.
*
* @param person the expected person.
* @return a new {@link IsEqualToEntity} matcher with the provided
* {@link Person} as the expected value.
*/
@Factory
public static IsEqualToPerson equalsToPerson(Person person) {
return new IsEqualToPerson(person);
}
/**
* Factory method that returns a new {@link Matcher} that includes several
* {@link IsEqualToPerson} matchers, each one using an {@link Person} of the
* provided ones as the expected value.
*
* @param persons the persons to be used as the expected values.
* @return a new {@link Matcher} that includes several
* {@link IsEqualToPerson} matchers, each one using an {@link Person} of the
* provided ones as the expected value.
* @see IsEqualToEntity#containsEntityInAnyOrder(java.util.function.Function, Object...)
*/
@Factory
public static Matcher<Iterable<? extends Person>> containsPeopleInAnyOrder(Person ... persons) {
return containsEntityInAnyOrder(IsEqualToPerson::equalsToPerson, persons);
}
}
package es.uvigo.esei.daa.matchers;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import es.uvigo.esei.daa.entities.Person;
import es.uvigo.esei.daa.entities.User;
public class IsEqualToUser extends IsEqualToEntity<User> {
public IsEqualToUser(User entity) {
super(entity);
}
@Override
protected boolean matchesSafely(User actual) {
this.clearDescribeTo();
if (actual == null) {
this.addTemplatedDescription("actual", expected.toString());
return false;
} else {
return checkAttribute("login", User::getLogin, actual)
&& checkAttribute("password", User::getPassword, actual);
}
}
/**
* Factory method that creates a new {@link IsEqualToEntity} matcher with
* the provided {@link Person} as the expected value.
*
* @param user the expected person.
* @return a new {@link IsEqualToEntity} matcher with the provided
* {@link Person} as the expected value.
*/
@Factory
public static IsEqualToUser equalsToUser(User user) {
return new IsEqualToUser(user);
}
/**
* Factory method that returns a new {@link Matcher} that includes several
* {@link IsEqualToUser} matchers, each one using an {@link Person} of the
* provided ones as the expected value.
*
* @param users the persons to be used as the expected values.
* @return a new {@link Matcher} that includes several
* {@link IsEqualToUser} matchers, each one using an {@link Person} of the
* provided ones as the expected value.
* @see IsEqualToEntity#containsEntityInAnyOrder(java.util.function.Function, Object...)
*/
@Factory
public static Matcher<Iterable<? extends User>> containsPeopleInAnyOrder(User ... users) {
return containsEntityInAnyOrder(IsEqualToUser::equalsToUser, users);
}
}
package es.uvigo.esei.daa.rest;
import static es.uvigo.esei.daa.dataset.PeopleDataset.existentId;
import static es.uvigo.esei.daa.dataset.PeopleDataset.existentPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newName;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newSurname;
import static es.uvigo.esei.daa.dataset.PeopleDataset.nonExistentId;
import static es.uvigo.esei.daa.dataset.PeopleDataset.people;
import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasBadRequestStatus;
import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasOkStatus;
import static es.uvigo.esei.daa.matchers.IsEqualToPerson.containsPeopleInAnyOrder;
import static es.uvigo.esei.daa.matchers.IsEqualToPerson.equalsToPerson;
import static javax.ws.rs.client.Entity.entity;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.util.List;
import javax.sql.DataSource;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import es.uvigo.esei.daa.DAAExampleApplication;
import es.uvigo.esei.daa.entities.Person;
import es.uvigo.esei.daa.listeners.ApplicationContextBinding;
import es.uvigo.esei.daa.listeners.ApplicationContextJndiBindingTestExecutionListener;
import es.uvigo.esei.daa.listeners.DbManagement;
import es.uvigo.esei.daa.listeners.DbManagementTestExecutionListener;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:contexts/mem-context.xml")
@TestExecutionListeners({
DbUnitTestExecutionListener.class,
DbManagementTestExecutionListener.class,
ApplicationContextJndiBindingTestExecutionListener.class
})
@ApplicationContextBinding(
jndiUrl = "java:/comp/env/jdbc/daaexample",
type = DataSource.class
)
@DbManagement(
create = "classpath:db/hsqldb.sql",
drop = "classpath:db/hsqldb-drop.sql"
)
@DatabaseSetup("/datasets/dataset.xml")
@ExpectedDatabase("/datasets/dataset.xml")
public class PeopleResourceTest extends JerseyTest {
@Override
protected Application configure() {
return new DAAExampleApplication();
}
@Override
protected void configureClient(ClientConfig config) {
super.configureClient(config);
// Enables JSON transformation in client
config.register(JacksonJsonProvider.class);
config.property("com.sun.jersey.api.json.POJOMappingFeature", Boolean.TRUE);
}
@Test
public void testList() throws IOException {
final Response response = target("people").request().get();
assertThat(response, hasOkStatus());
final List<Person> people = response.readEntity(new GenericType<List<Person>>(){});
assertThat(people, containsPeopleInAnyOrder(people()));
}
@Test
public void testGet() throws IOException {
final Response response = target("people/" + existentId()).request().get();
assertThat(response, hasOkStatus());
final Person person = response.readEntity(Person.class);
assertThat(person, is(equalsToPerson(existentPerson())));
}
@Test
public void testGetInvalidId() throws IOException {
final Response response = target("people/" + nonExistentId()).request().get();
assertThat(response, hasBadRequestStatus());
}
@Test
@ExpectedDatabase("/datasets/dataset-add.xml")
public void testAdd() throws IOException {
final Form form = new Form();
form.param("name", newName());
form.param("surname", newSurname());
final Response response = target("people")
.request(MediaType.APPLICATION_JSON_TYPE)
.post(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
assertThat(response, hasOkStatus());
final Person person = response.readEntity(Person.class);
assertThat(person, is(equalsToPerson(newPerson())));
}
@Test
public void testAddMissingName() throws IOException {
final Form form = new Form();
form.param("surname", newSurname());
final Response response = target("people")
.request(MediaType.APPLICATION_JSON_TYPE)
.post(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
assertThat(response, hasBadRequestStatus());
}
@Test
public void testAddMissingSurname() throws IOException {
final Form form = new Form();
form.param("name", newName());
final Response response = target("people")
.request(MediaType.APPLICATION_JSON_TYPE)
.post(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
assertThat(response, hasBadRequestStatus());
}
@Test
@ExpectedDatabase("/datasets/dataset-modify.xml")
public void testModify() throws IOException {
final Form form = new Form();
form.param("name", newName());
form.param("surname", newSurname());
final Response response = target("people/" + existentId())
.request(MediaType.APPLICATION_JSON_TYPE)
.put(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
assertThat(response, hasOkStatus());
final Person modifiedPerson = response.readEntity(Person.class);
final Person person = existentPerson();
person.setName(newName());
person.setSurname(newSurname());
assertThat(modifiedPerson, is(equalsToPerson(person)));
}
@Test
public void testModifyName() throws IOException {
final Form form = new Form();
form.param("name", newName());
final Response response = target("people/" + existentId())
.request(MediaType.APPLICATION_JSON_TYPE)
.put(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
assertThat(response, hasBadRequestStatus());
}
@Test
public void testModifySurname() throws IOException {
final Form form = new Form();
form.param("surname", newSurname());
final Response response = target("people/" + existentId())
.request(MediaType.APPLICATION_JSON_TYPE)
.put(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
assertThat(response, hasBadRequestStatus());
}
@Test
public void testModifyInvalidId() throws IOException {
final Form form = new Form();
form.param("name", newName());
form.param("surname", newSurname());
final Response response = target("people/" + nonExistentId())
.request(MediaType.APPLICATION_JSON_TYPE)
.put(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
assertThat(response, hasBadRequestStatus());
}
@Test
@ExpectedDatabase("/datasets/dataset-delete.xml")
public void testDelete() throws IOException {
final Response response = target("people/" + existentId()).request().delete();
assertThat(response, hasOkStatus());
final Integer deletedId = response.readEntity(Integer.class);
assertThat(deletedId, is(equalTo(existentId())));
}
@Test
public void testDeleteInvalidId() throws IOException {
final Response response = target("people/" + nonExistentId()).request().delete();
assertThat(response, hasBadRequestStatus());
}
}
package es.uvigo.esei.daa.rest;
import static es.uvigo.esei.daa.dataset.PeopleDataset.existentId;
import static es.uvigo.esei.daa.dataset.PeopleDataset.existentPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newName;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newPerson;
import static es.uvigo.esei.daa.dataset.PeopleDataset.newSurname;
import static es.uvigo.esei.daa.dataset.PeopleDataset.people;
import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasBadRequestStatus;
import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasInternalServerErrorStatus;
import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasOkStatus;
import static es.uvigo.esei.daa.matchers.IsEqualToPerson.containsPeopleInAnyOrder;
import static es.uvigo.esei.daa.matchers.IsEqualToPerson.equalsToPerson;
import static java.util.Arrays.asList;
import static org.easymock.EasyMock.anyInt;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.anyString;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.util.List;
import javax.ws.rs.core.Response;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import es.uvigo.esei.daa.dao.DAOException;
import es.uvigo.esei.daa.dao.PeopleDAO;
import es.uvigo.esei.daa.entities.Person;
public class PeopleResourceUnitTest {
private PeopleDAO daoMock;
private PeopleResource resource;
@Before
public void setUp() throws Exception {
daoMock = createMock(PeopleDAO.class);
resource = new PeopleResource(daoMock);
}
@After
public void tearDown() throws Exception {
try {
verify(daoMock);
} finally {
daoMock = null;
resource = null;
}
}
@Test
@SuppressWarnings("unchecked")
public void testList() throws Exception {
final List<Person> people = asList(people());
expect(daoMock.list()).andReturn(people);
replay(daoMock);
final Response response = resource.list();
assertThat(response, hasOkStatus());
assertThat((List<Person>) response.getEntity(), containsPeopleInAnyOrder(people()));
}
@Test
public void testListDAOException() throws Exception {
expect(daoMock.list()).andThrow(new DAOException());
replay(daoMock);
final Response response = resource.list();
assertThat(response, hasInternalServerErrorStatus());
}
@Test
public void testGet() throws Exception {
final Person person = existentPerson();
expect(daoMock.get(person.getId())).andReturn(person);
replay(daoMock);
final Response response = resource.get(person.getId());
assertThat(response, hasOkStatus());
assertThat((Person) response.getEntity(), is(equalsToPerson(person)));
}
@Test
public void testGetDAOException() throws Exception {
expect(daoMock.get(anyInt())).andThrow(new DAOException());
replay(daoMock);
final Response response = resource.get(existentId());
assertThat(response, hasInternalServerErrorStatus());
}
@Test
public void testGetIllegalArgumentException() throws Exception {
expect(daoMock.get(anyInt())).andThrow(new IllegalArgumentException());
replay(daoMock);
final Response response = resource.get(existentId());
assertThat(response, hasBadRequestStatus());
}
@Test
public void testDelete() throws Exception {
daoMock.delete(anyInt());
replay(daoMock);
final Response response = resource.delete(1);
assertThat(response, hasOkStatus());
}
@Test
public void testDeleteDAOException() throws Exception {
daoMock.delete(anyInt());
expectLastCall().andThrow(new DAOException());
replay(daoMock);
final Response response = resource.delete(1);
assertThat(response, hasInternalServerErrorStatus());
}
@Test
public void testDeleteIllegalArgumentException() throws Exception {
daoMock.delete(anyInt());
expectLastCall().andThrow(new IllegalArgumentException());
replay(daoMock);
final Response response = resource.delete(1);
assertThat(response, hasBadRequestStatus());
}
@Test
public void testModify() throws Exception {
final Person person = existentPerson();
person.setName(newName());
person.setSurname(newSurname());
daoMock.modify(person);
replay(daoMock);
final Response response = resource.modify(
person.getId(), person.getName(), person.getSurname());
assertThat(response, hasOkStatus());
assertEquals(person, response.getEntity());
}
@Test
public void testModifyDAOException() throws Exception {
daoMock.modify(anyObject());
expectLastCall().andThrow(new DAOException());
replay(daoMock);
final Response response = resource.modify(existentId(), newName(), newSurname());
assertThat(response, hasInternalServerErrorStatus());
}
@Test
public void testModifyIllegalArgumentException() throws Exception {
daoMock.modify(anyObject());
expectLastCall().andThrow(new IllegalArgumentException());
replay(daoMock);
final Response response = resource.modify(existentId(), newName(), newSurname());
assertThat(response, hasBadRequestStatus());
}
@Test
public void testModifyNullPointerException() throws Exception {
daoMock.modify(anyObject());
expectLastCall().andThrow(new NullPointerException());
replay(daoMock);
final Response response = resource.modify(existentId(), newName(), newSurname());
assertThat(response, hasBadRequestStatus());
}
@Test
public void testAdd() throws Exception {
expect(daoMock.add(newName(), newSurname()))
.andReturn(newPerson());
replay(daoMock);
final Response response = resource.add(newName(), newSurname());
assertThat(response, hasOkStatus());
assertThat((Person) response.getEntity(), is(equalsToPerson(newPerson())));
}
@Test
public void testAddDAOException() throws Exception {
expect(daoMock.add(anyString(), anyString()))
.andThrow(new DAOException());
replay(daoMock);
final Response response = resource.add(newName(), newSurname());
assertThat(response, hasInternalServerErrorStatus());
}
@Test
public void testAddIllegalArgumentException() throws Exception {
expect(daoMock.add(anyString(), anyString()))
.andThrow(new IllegalArgumentException());
replay(daoMock);
final Response response = resource.add(newName(), newSurname());
assertThat(response, hasBadRequestStatus());
}
}
This diff is collapsed.
package es.uvigo.esei.daa.suites;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import es.uvigo.esei.daa.web.PeopleWebTest;
@SuiteClasses({
PeopleWebTest.class
})
@RunWith(Suite.class)
public class AcceptanceTestSuite {
}
package es.uvigo.esei.daa.suites;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import es.uvigo.esei.daa.dao.PeopleDAOTest;
import es.uvigo.esei.daa.rest.PeopleResourceTest;
import es.uvigo.esei.daa.rest.UsersResourceTest;
@SuiteClasses({
PeopleDAOTest.class,
PeopleResourceTest.class,
UsersResourceTest.class
})
@RunWith(Suite.class)
public class IntegrationTestSuite {
}
package es.uvigo.esei.daa.suites;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import es.uvigo.esei.daa.dao.PeopleDAOUnitTest;
import es.uvigo.esei.daa.entities.PersonUnitTest;
import es.uvigo.esei.daa.rest.PeopleResourceUnitTest;
@SuiteClasses({
PersonUnitTest.class,
PeopleDAOUnitTest.class,
PeopleResourceUnitTest.class
})
@RunWith(Suite.class)
public class UnitTestSuite {
}
package es.uvigo.esei.daa.util;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.support.ui.ExpectedCondition;
/*
* Implementation based on https://stackoverflow.com/questions/33348600/selenium-wait-for-ajax-content-to-load-universal-approach
*/
public class AdditionalConditions {
public static ExpectedCondition<Boolean> jQueryAjaxCallsHaveCompleted() {
return driver ->
(Boolean) ((JavascriptExecutor) driver).executeScript("return (window.jQuery !== null) && (jQuery.active === 0)");
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment