Init rest api

This commit is contained in:
Blizzard Finnegan 2023-09-06 20:09:42 -04:00
parent d7f3ce80a1
commit 3cbabfec12
Signed by: blizzardfinnegan
GPG key ID: 33BC649AD444BD96
25 changed files with 1397 additions and 0 deletions

Binary file not shown.

View file

@ -0,0 +1,22 @@
# Read Me First
The following was discovered as part of building this project:
* The original package name 'com.heroes.api.heroes-api' is invalid and this project uses 'com.heroes.api.heroesapi' instead.
# Getting Started
### Reference Documentation
For further reference, please consider the following sections:
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.5.6/maven-plugin/reference/html/)
* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.5.6/maven-plugin/reference/html/#build-image)
* [Spring Web](https://docs.spring.io/spring-boot/docs/2.5.6/reference/htmlsingle/#boot-features-developing-web-applications)
### Guides
The following guides illustrate how to use some features concretely:
* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
* [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/)

View file

@ -0,0 +1,42 @@
[
{
"id": 11,
"name": "Mr. Nice"
},
{
"id": 12,
"name": "Narco"
},
{
"id": 13,
"name": "Bombasto"
},
{
"id": 14,
"name": "Celeritas"
},
{
"id": 15,
"name": "Magneta"
},
{
"id": 16,
"name": "RubberMan"
},
{
"id": 17,
"name": "Dynama"
},
{
"id": 18,
"name": "Dr IQ"
},
{
"id": 19,
"name": "Magma"
},
{
"id": 20,
"name": "Tornado"
}
]

View file

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.heroes.api</groupId>
<artifactId>heroes-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>heroes-api</name>
<description>API for Tour of Heroes</description>
<properties>
<java.version>11</java.version>
<jacoco.version>0.8.7</jacoco.version>
<maven.exec.version>3.0.0</maven.exec.version>
<maven.assembly.version>3.1.0</maven.assembly.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${maven.exec.version}</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
<execution>
<id>tests-and-coverage</id>
<configuration>
<executable>mvn</executable>
<arguments>
<argument>clean</argument>
<argument>test-compile</argument>
<argument>surefire:test@controller</argument>
<argument>jacoco:report@controller</argument>
<argument>surefire:test@model</argument>
<argument>jacoco:report@model</argument>
<argument>surefire:test@persistence</argument>
<argument>jacoco:report@persistence</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>zip</id>
<configuration>
<executable>mvn</executable>
<arguments>
<argument>assembly:single@zip</argument>
</arguments>
</configuration>
</execution>
</executions>
<configuration>
<mainClass>com.heroes.api.heroesapi.HeroesApiApplication</mainClass>
<arguments></arguments>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<configuration>
<destfile>/target/coverage-reports/jacoco-unit.exec</destfile>
<datafile>/target/coverage-reports/jacoco-unit.exec</datafile>
</configuration>
<executions>
<execution>
<id>jacoco-initialize</id>
<configuration>
<!-- throw away the old data with each test run -->
<append>false</append>
</configuration>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- attached to Maven test phase -->
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>controller</id>
<configuration>
<footer>Controller Tier</footer>
<title>Heroes API Controller Tier Test Coverage</title>
<outputDirectory>target/site/jacoco/controller</outputDirectory>
</configuration>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>persistence</id>
<configuration>
<footer>Persistence Tier</footer>
<title>Heroes API Persistence Tier Test Coverage</title>
<outputDirectory>target/site/jacoco/persistence</outputDirectory>
</configuration>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>model</id>
<configuration>
<footer>Model Tier</footer>
<title>Heroes API Model Tier Test Coverage</title>
<outputDirectory>target/site/jacoco/model</outputDirectory>
</configuration>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.90</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven.assembly.version}</version>
<executions>
<execution>
<id>zip</id>
<configuration>
<descriptors>
<descriptor>src/assembly/zip.xml</descriptor>
</descriptors>
<finalName>heroes-api</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,26 @@
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/3.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/3.1.0 http://maven.apache.org/xsd/assembly-3.1.0.xsd">
<id>no-tests</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<useDefaultExcludes>false</useDefaultExcludes>
<includes>
<include>HELP.md</include>
<include>pom.xml</include>
</includes>
</fileSet>
<fileSet>
<includes>
<include>src/**</include>
<include>src/assembly/zip.xml</include>
<include>data/**</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View file

@ -0,0 +1,13 @@
package com.heroes.api.heroesapi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HeroesApiApplication {
public static void main(String[] args) {
SpringApplication.run(HeroesApiApplication.class, args);
}
}

View file

@ -0,0 +1,16 @@
package com.heroes.api.heroesapi;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH");
}
}

View file

@ -0,0 +1,159 @@
package com.heroes.api.heroesapi.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.heroes.api.heroesapi.persistence.HeroDAO;
import com.heroes.api.heroesapi.model.Hero;
/**
* Handles the REST API requests for the Hero resource
* <p>
* {@literal @}RestController Spring annotation identifies this class as a REST API
* method handler to the Spring framework
*
* @author SWEN Faculty
*/
@RestController
@RequestMapping("heroes")
public class HeroController {
private static final Logger LOG = Logger.getLogger(HeroController.class.getName());
private HeroDAO heroDao;
/**
* Creates a REST API controller to reponds to requests
*
* @param heroDao The {@link HeroDAO Hero Data Access Object} to perform CRUD operations
* <br>
* This dependency is injected by the Spring Framework
*/
public HeroController(HeroDAO heroDao) {
this.heroDao = heroDao;
}
/**
* Responds to the GET request for a {@linkplain Hero hero} for the given id
*
* @param id The id used to locate the {@link Hero hero}
*
* @return ResponseEntity with {@link Hero hero} object and HTTP status of OK if found<br>
* ResponseEntity with HTTP status of NOT_FOUND if not found<br>
* ResponseEntity with HTTP status of INTERNAL_SERVER_ERROR otherwise
*/
@GetMapping("/{id}")
public ResponseEntity<Hero> getHero(@PathVariable int id) {
LOG.info("GET /heroes/" + id);
try {
Hero hero = heroDao.getHero(id);
if (hero != null)
return new ResponseEntity<Hero>(hero,HttpStatus.OK);
else
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
catch(IOException e) {
LOG.log(Level.SEVERE,e.getLocalizedMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* Responds to the GET request for all {@linkplain Hero heroes}
*
* @return ResponseEntity with array of {@link Hero hero} objects (may be empty) and
* HTTP status of OK<br>
* ResponseEntity with HTTP status of INTERNAL_SERVER_ERROR otherwise
*/
@GetMapping("")
public ResponseEntity<Hero[]> getHeroes() {
LOG.info("GET /heroes");
// Replace below with your implementation
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* Responds to the GET request for all {@linkplain Hero heroes} whose name contains
* the text in name
*
* @param name The name parameter which contains the text used to find the {@link Hero heroes}
*
* @return ResponseEntity with array of {@link Hero hero} objects (may be empty) and
* HTTP status of OK<br>
* ResponseEntity with HTTP status of INTERNAL_SERVER_ERROR otherwise
* <p>
* Example: Find all heroes that contain the text "ma"
* GET http://localhost:8080/heroes/?name=ma
*/
@GetMapping("/")
public ResponseEntity<Hero[]> searchHeroes(@RequestParam String name) {
LOG.info("GET /heroes/?name="+name);
// Replace below with your implementation
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* Creates a {@linkplain Hero hero} with the provided hero object
*
* @param hero - The {@link Hero hero} to create
*
* @return ResponseEntity with created {@link Hero hero} object and HTTP status of CREATED<br>
* ResponseEntity with HTTP status of CONFLICT if {@link Hero hero} object already exists<br>
* ResponseEntity with HTTP status of INTERNAL_SERVER_ERROR otherwise
*/
@PostMapping("")
public ResponseEntity<Hero> createHero(@RequestBody Hero hero) {
LOG.info("POST /heroes " + hero);
// Replace below with your implementation
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* Updates the {@linkplain Hero hero} with the provided {@linkplain Hero hero} object, if it exists
*
* @param hero The {@link Hero hero} to update
*
* @return ResponseEntity with updated {@link Hero hero} object and HTTP status of OK if updated<br>
* ResponseEntity with HTTP status of NOT_FOUND if not found<br>
* ResponseEntity with HTTP status of INTERNAL_SERVER_ERROR otherwise
*/
@PutMapping("")
public ResponseEntity<Hero> updateHero(@RequestBody Hero hero) {
LOG.info("PUT /heroes " + hero);
// Replace below with your implementation
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* Deletes a {@linkplain Hero hero} with the given id
*
* @param id The id of the {@link Hero hero} to deleted
*
* @return ResponseEntity HTTP status of OK if deleted<br>
* ResponseEntity with HTTP status of NOT_FOUND if not found<br>
* ResponseEntity with HTTP status of INTERNAL_SERVER_ERROR otherwise
*/
@DeleteMapping("/{id}")
public ResponseEntity<Hero> deleteHero(@PathVariable int id) {
LOG.info("DELETE /heroes/" + id);
// Replace below with your implementation
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}

View file

@ -0,0 +1,60 @@
package com.heroes.api.heroesapi.model;
import java.util.logging.Logger;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Represents a Hero entity
*
* @author SWEN Faculty
*/
public class Hero {
private static final Logger LOG = Logger.getLogger(Hero.class.getName());
// Package private for tests
static final String STRING_FORMAT = "Hero [id=%d, name=%s]";
@JsonProperty("id") private int id;
@JsonProperty("name") private String name;
/**
* Create a hero with the given id and name
* @param id The id of the hero
* @param name The name of the hero
*
* {@literal @}JsonProperty is used in serialization and deserialization
* of the JSON object to the Java object in mapping the fields. If a field
* is not provided in the JSON object, the Java field gets the default Java
* value, i.e. 0 for int
*/
public Hero(@JsonProperty("id") int id, @JsonProperty("name") String name) {
this.id = id;
this.name = name;
}
/**
* Retrieves the id of the hero
* @return The id of the hero
*/
public int getId() {return id;}
/**
* Sets the name of the hero - necessary for JSON object to Java object deserialization
* @param name The name of the hero
*/
public void setName(String name) {this.name = name;}
/**
* Retrieves the name of the hero
* @return The name of the hero
*/
public String getName() {return name;}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return String.format(STRING_FORMAT,id,name);
}
}

View file

@ -0,0 +1,82 @@
package com.heroes.api.heroesapi.persistence;
import java.io.IOException;
import com.heroes.api.heroesapi.model.Hero;
/**
* Defines the interface for Hero object persistence
*
* @author SWEN Faculty
*/
public interface HeroDAO {
/**
* Retrieves all {@linkplain Hero heroes}
*
* @return An array of {@link Hero hero} objects, may be empty
*
* @throws IOException if an issue with underlying storage
*/
Hero[] getHeroes() throws IOException;
/**
* Finds all {@linkplain Hero heroes} whose name contains the given text
*
* @param containsText The text to match against
*
* @return An array of {@link Hero heroes} whose nemes contains the given text, may be empty
*
* @throws IOException if an issue with underlying storage
*/
Hero[] findHeroes(String containsText) throws IOException;
/**
* Retrieves a {@linkplain Hero hero} with the given id
*
* @param id The id of the {@link Hero hero} to get
*
* @return a {@link Hero hero} object with the matching id
* <br>
* null if no {@link Hero hero} with a matching id is found
*
* @throws IOException if an issue with underlying storage
*/
Hero getHero(int id) throws IOException;
/**
* Creates and saves a {@linkplain Hero hero}
*
* @param hero {@linkplain Hero hero} object to be created and saved
* <br>
* The id of the hero object is ignored and a new uniqe id is assigned
*
* @return new {@link Hero hero} if successful, false otherwise
*
* @throws IOException if an issue with underlying storage
*/
Hero createHero(Hero hero) throws IOException;
/**
* Updates and saves a {@linkplain Hero hero}
*
* @param {@link Hero hero} object to be updated and saved
*
* @return updated {@link Hero hero} if successful, null if
* {@link Hero hero} could not be found
*
* @throws IOException if underlying storage cannot be accessed
*/
Hero updateHero(Hero hero) throws IOException;
/**
* Deletes a {@linkplain Hero hero} with the given id
*
* @param id The id of the {@link Hero hero}
*
* @return true if the {@link Hero hero} was deleted
* <br>
* false if hero with the given id does not exist
*
* @throws IOException if underlying storage cannot be accessed
*/
boolean deleteHero(int id) throws IOException;
}

View file

@ -0,0 +1,217 @@
package com.heroes.api.heroesapi.persistence;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.heroes.api.heroesapi.model.Hero;
/**
* Implements the functionality for JSON file-based peristance for Heroes
*
* {@literal @}Component Spring annotation instantiates a single instance of this
* class and injects the instance into other classes as needed
*
* @author SWEN Faculty
*/
@Component
public class HeroFileDAO implements HeroDAO {
private static final Logger LOG = Logger.getLogger(HeroFileDAO.class.getName());
Map<Integer,Hero> heroes; // Provides a local cache of the hero objects
// so that we don't need to read from the file
// each time
private ObjectMapper objectMapper; // Provides conversion between Hero
// objects and JSON text format written
// to the file
private static int nextId; // The next Id to assign to a new hero
private String filename; // Filename to read from and write to
/**
* Creates a Hero File Data Access Object
*
* @param filename Filename to read from and write to
* @param objectMapper Provides JSON Object to/from Java Object serialization and deserialization
*
* @throws IOException when file cannot be accessed or read from
*/
public HeroFileDAO(@Value("${heroes.file}") String filename,ObjectMapper objectMapper) throws IOException {
this.filename = filename;
this.objectMapper = objectMapper;
load(); // load the heroes from the file
}
/**
* Generates the next id for a new {@linkplain Hero hero}
*
* @return The next id
*/
private synchronized static int nextId() {
int id = nextId;
++nextId;
return id;
}
/**
* Generates an array of {@linkplain Hero heroes} from the tree map
*
* @return The array of {@link Hero heroes}, may be empty
*/
private Hero[] getHeroesArray() {
return getHeroesArray(null);
}
/**
* Generates an array of {@linkplain Hero heroes} from the tree map for any
* {@linkplain Hero heroes} that contains the text specified by containsText
* <br>
* If containsText is null, the array contains all of the {@linkplain Hero heroes}
* in the tree map
*
* @return The array of {@link Hero heroes}, may be empty
*/
private Hero[] getHeroesArray(String containsText) { // if containsText == null, no filter
ArrayList<Hero> heroArrayList = new ArrayList<>();
for (Hero hero : heroes.values()) {
if (containsText == null || hero.getName().contains(containsText)) {
heroArrayList.add(hero);
}
}
Hero[] heroArray = new Hero[heroArrayList.size()];
heroArrayList.toArray(heroArray);
return heroArray;
}
/**
* Saves the {@linkplain Hero heroes} from the map into the file as an array of JSON objects
*
* @return true if the {@link Hero heroes} were written successfully
*
* @throws IOException when file cannot be accessed or written to
*/
private boolean save() throws IOException {
Hero[] heroArray = getHeroesArray();
// Serializes the Java Objects to JSON objects into the file
// writeValue will thrown an IOException if there is an issue
// with the file or reading from the file
objectMapper.writeValue(new File(filename),heroArray);
return true;
}
/**
* Loads {@linkplain Hero heroes} from the JSON file into the map
* <br>
* Also sets next id to one more than the greatest id found in the file
*
* @return true if the file was read successfully
*
* @throws IOException when file cannot be accessed or read from
*/
private boolean load() throws IOException {
heroes = new TreeMap<>();
nextId = 0;
// Deserializes the JSON objects from the file into an array of heroes
// readValue will throw an IOException if there's an issue with the file
// or reading from the file
Hero[] heroArray = objectMapper.readValue(new File(filename),Hero[].class);
// Add each hero to the tree map and keep track of the greatest id
for (Hero hero : heroArray) {
heroes.put(hero.getId(),hero);
if (hero.getId() > nextId)
nextId = hero.getId();
}
// Make the next id one greater than the maximum from the file
++nextId;
return true;
}
/**
** {@inheritDoc}
*/
@Override
public Hero[] getHeroes() {
synchronized(heroes) {
return getHeroesArray();
}
}
/**
** {@inheritDoc}
*/
@Override
public Hero[] findHeroes(String containsText) {
synchronized(heroes) {
return getHeroesArray(containsText);
}
}
/**
** {@inheritDoc}
*/
@Override
public Hero getHero(int id) {
synchronized(heroes) {
if (heroes.containsKey(id))
return heroes.get(id);
else
return null;
}
}
/**
** {@inheritDoc}
*/
@Override
public Hero createHero(Hero hero) throws IOException {
synchronized(heroes) {
// We create a new hero object because the id field is immutable
// and we need to assign the next unique id
Hero newHero = new Hero(nextId(),hero.getName());
heroes.put(newHero.getId(),newHero);
save(); // may throw an IOException
return newHero;
}
}
/**
** {@inheritDoc}
*/
@Override
public Hero updateHero(Hero hero) throws IOException {
synchronized(heroes) {
if (heroes.containsKey(hero.getId()) == false)
return null; // hero does not exist
heroes.put(hero.getId(),hero);
save(); // may throw an IOException
return hero;
}
}
/**
** {@inheritDoc}
*/
@Override
public boolean deleteHero(int id) throws IOException {
synchronized(heroes) {
if (heroes.containsKey(id)) {
heroes.remove(id);
return save();
}
else
return false;
}
}
}

View file

@ -0,0 +1,2 @@
server.error.include-message=always
heroes.file=data/heroes.json

View file

@ -0,0 +1,18 @@
package com.heroes.api.heroesapi;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
/**
* This is a built in test that comes with the Spring framework that validates
* that the REST API service starts (that's it)
*/
@Tag("Controller-tier")
@SpringBootTest
class HeroesApiApplicationTests {
@Test
void testContextLoads() {
}
}

View file

@ -0,0 +1,285 @@
package com.heroes.api.heroesapi.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import com.heroes.api.heroesapi.persistence.HeroDAO;
import com.heroes.api.heroesapi.model.Hero;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* Test the Hero Controller class
*
* @author SWEN Faculty
*/
@Tag("Controller-tier")
public class HeroControllerTest {
private HeroController heroController;
private HeroDAO mockHeroDAO;
/**
* Before each test, create a new HeroController object and inject
* a mock Hero DAO
*/
@BeforeEach
public void setupHeroController() {
mockHeroDAO = mock(HeroDAO.class);
heroController = new HeroController(mockHeroDAO);
}
@Test
public void testGetHero() throws IOException { // getHero may throw IOException
// Setup
Hero hero = new Hero(99,"Galactic Agent");
// When the same id is passed in, our mock Hero DAO will return the Hero object
when(mockHeroDAO.getHero(hero.getId())).thenReturn(hero);
// Invoke
ResponseEntity<Hero> response = heroController.getHero(hero.getId());
// Analyze
assertEquals(HttpStatus.OK,response.getStatusCode());
assertEquals(hero,response.getBody());
}
@Test
public void testGetHeroNotFound() throws Exception { // createHero may throw IOException
// Setup
int heroId = 99;
// When the same id is passed in, our mock Hero DAO will return null, simulating
// no hero found
when(mockHeroDAO.getHero(heroId)).thenReturn(null);
// Invoke
ResponseEntity<Hero> response = heroController.getHero(heroId);
// Analyze
assertEquals(HttpStatus.NOT_FOUND,response.getStatusCode());
}
@Test
public void testGetHeroHandleException() throws Exception { // createHero may throw IOException
// Setup
int heroId = 99;
// When getHero is called on the Mock Hero DAO, throw an IOException
doThrow(new IOException()).when(mockHeroDAO).getHero(heroId);
// Invoke
ResponseEntity<Hero> response = heroController.getHero(heroId);
// Analyze
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR,response.getStatusCode());
}
/*****************************************************************
* The following tests will fail until all HeroController methods
* are implemented.
****************************************************************/
@Test
public void testCreateHero() throws IOException { // createHero may throw IOException
// Setup
Hero hero = new Hero(99,"Wi-Fire");
// when createHero is called, return true simulating successful
// creation and save
when(mockHeroDAO.createHero(hero)).thenReturn(hero);
// Invoke
ResponseEntity<Hero> response = heroController.createHero(hero);
// Analyze
assertEquals(HttpStatus.CREATED,response.getStatusCode());
assertEquals(hero,response.getBody());
}
@Test
public void testCreateHeroFailed() throws IOException { // createHero may throw IOException
// Setup
Hero hero = new Hero(99,"Bolt");
// when createHero is called, return false simulating failed
// creation and save
when(mockHeroDAO.createHero(hero)).thenReturn(null);
// Invoke
ResponseEntity<Hero> response = heroController.createHero(hero);
// Analyze
assertEquals(HttpStatus.CONFLICT,response.getStatusCode());
}
@Test
public void testCreateHeroHandleException() throws IOException { // createHero may throw IOException
// Setup
Hero hero = new Hero(99,"Ice Gladiator");
// When createHero is called on the Mock Hero DAO, throw an IOException
doThrow(new IOException()).when(mockHeroDAO).createHero(hero);
// Invoke
ResponseEntity<Hero> response = heroController.createHero(hero);
// Analyze
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR,response.getStatusCode());
}
@Test
public void testUpdateHero() throws IOException { // updateHero may throw IOException
// Setup
Hero hero = new Hero(99,"Wi-Fire");
// when updateHero is called, return true simulating successful
// update and save
when(mockHeroDAO.updateHero(hero)).thenReturn(hero);
ResponseEntity<Hero> response = heroController.updateHero(hero);
hero.setName("Bolt");
// Invoke
response = heroController.updateHero(hero);
// Analyze
assertEquals(HttpStatus.OK,response.getStatusCode());
assertEquals(hero,response.getBody());
}
@Test
public void testUpdateHeroFailed() throws IOException { // updateHero may throw IOException
// Setup
Hero hero = new Hero(99,"Galactic Agent");
// when updateHero is called, return true simulating successful
// update and save
when(mockHeroDAO.updateHero(hero)).thenReturn(null);
// Invoke
ResponseEntity<Hero> response = heroController.updateHero(hero);
// Analyze
assertEquals(HttpStatus.NOT_FOUND,response.getStatusCode());
}
@Test
public void testUpdateHeroHandleException() throws IOException { // updateHero may throw IOException
// Setup
Hero hero = new Hero(99,"Galactic Agent");
// When updateHero is called on the Mock Hero DAO, throw an IOException
doThrow(new IOException()).when(mockHeroDAO).updateHero(hero);
// Invoke
ResponseEntity<Hero> response = heroController.updateHero(hero);
// Analyze
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR,response.getStatusCode());
}
@Test
public void testGetHeroes() throws IOException { // getHeroes may throw IOException
// Setup
Hero[] heroes = new Hero[2];
heroes[0] = new Hero(99,"Bolt");
heroes[1] = new Hero(100,"The Great Iguana");
// When getHeroes is called return the heroes created above
when(mockHeroDAO.getHeroes()).thenReturn(heroes);
// Invoke
ResponseEntity<Hero[]> response = heroController.getHeroes();
// Analyze
assertEquals(HttpStatus.OK,response.getStatusCode());
assertEquals(heroes,response.getBody());
}
@Test
public void testGetHeroesHandleException() throws IOException { // getHeroes may throw IOException
// Setup
// When getHeroes is called on the Mock Hero DAO, throw an IOException
doThrow(new IOException()).when(mockHeroDAO).getHeroes();
// Invoke
ResponseEntity<Hero[]> response = heroController.getHeroes();
// Analyze
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR,response.getStatusCode());
}
@Test
public void testSearchHeroes() throws IOException { // findHeroes may throw IOException
// Setup
String searchString = "la";
Hero[] heroes = new Hero[2];
heroes[0] = new Hero(99,"Galactic Agent");
heroes[1] = new Hero(100,"Ice Gladiator");
// When findHeroes is called with the search string, return the two
/// heroes above
when(mockHeroDAO.findHeroes(searchString)).thenReturn(heroes);
// Invoke
ResponseEntity<Hero[]> response = heroController.searchHeroes(searchString);
// Analyze
assertEquals(HttpStatus.OK,response.getStatusCode());
assertEquals(heroes,response.getBody());
}
@Test
public void testSearchHeroesHandleException() throws IOException { // findHeroes may throw IOException
// Setup
String searchString = "an";
// When createHero is called on the Mock Hero DAO, throw an IOException
doThrow(new IOException()).when(mockHeroDAO).findHeroes(searchString);
// Invoke
ResponseEntity<Hero[]> response = heroController.searchHeroes(searchString);
// Analyze
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR,response.getStatusCode());
}
@Test
public void testDeleteHero() throws IOException { // deleteHero may throw IOException
// Setup
int heroId = 99;
// when deleteHero is called return true, simulating successful deletion
when(mockHeroDAO.deleteHero(heroId)).thenReturn(true);
// Invoke
ResponseEntity<Hero> response = heroController.deleteHero(heroId);
// Analyze
assertEquals(HttpStatus.OK,response.getStatusCode());
}
@Test
public void testDeleteHeroNotFound() throws IOException { // deleteHero may throw IOException
// Setup
int heroId = 99;
// when deleteHero is called return false, simulating failed deletion
when(mockHeroDAO.deleteHero(heroId)).thenReturn(false);
// Invoke
ResponseEntity<Hero> response = heroController.deleteHero(heroId);
// Analyze
assertEquals(HttpStatus.NOT_FOUND,response.getStatusCode());
}
@Test
public void testDeleteHeroHandleException() throws IOException { // deleteHero may throw IOException
// Setup
int heroId = 99;
// When deleteHero is called on the Mock Hero DAO, throw an IOException
doThrow(new IOException()).when(mockHeroDAO).deleteHero(heroId);
// Invoke
ResponseEntity<Hero> response = heroController.deleteHero(heroId);
// Analyze
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR,response.getStatusCode());
}
}

View file

@ -0,0 +1,59 @@
package com.heroes.api.heroesapi.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* The unit test suite for the Hero class
*
* @author SWEN Faculty
*/
@Tag("Model-tier")
public class HeroTest {
@Test
public void testCtor() {
// Setup
int expected_id = 99;
String expected_name = "Wi-Fire";
// Invoke
Hero hero = new Hero(expected_id,expected_name);
// Analyze
assertEquals(expected_id,hero.getId());
assertEquals(expected_name,hero.getName());
}
@Test
public void testName() {
// Setup
int id = 99;
String name = "Wi-Fire";
Hero hero = new Hero(id,name);
String expected_name = "Galactic Agent";
// Invoke
hero.setName(expected_name);
// Analyze
assertEquals(expected_name,hero.getName());
}
@Test
public void testToString() {
// Setup
int id = 99;
String name = "Wi-Fire";
String expected_string = String.format(Hero.STRING_FORMAT,id,name);
Hero hero = new Hero(id,name);
// Invoke
String actual_string = hero.toString();
// Analyze
assertEquals(expected_string,actual_string);
}
}

View file

@ -0,0 +1,197 @@
package com.heroes.api.heroesapi.persistence;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heroes.api.heroesapi.model.Hero;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* Test the Hero File DAO class
*
* @author SWEN Faculty
*/
@Tag("Persistence-tier")
public class HeroFileDAOTest {
HeroFileDAO heroFileDAO;
Hero[] testHeroes;
ObjectMapper mockObjectMapper;
/**
* Before each test, we will create and inject a Mock Object Mapper to
* isolate the tests from the underlying file
* @throws IOException
*/
@BeforeEach
public void setupHeroFileDAO() throws IOException {
mockObjectMapper = mock(ObjectMapper.class);
testHeroes = new Hero[3];
testHeroes[0] = new Hero(99,"Wi-Fire");
testHeroes[1] = new Hero(100,"Galactic Agent");
testHeroes[2] = new Hero(101,"Ice Gladiator");
// When the object mapper is supposed to read from the file
// the mock object mapper will return the hero array above
when(mockObjectMapper
.readValue(new File("doesnt_matter.txt"),Hero[].class))
.thenReturn(testHeroes);
heroFileDAO = new HeroFileDAO("doesnt_matter.txt",mockObjectMapper);
}
@Test
public void testGetHeroes() {
// Invoke
Hero[] heroes = heroFileDAO.getHeroes();
// Analyze
assertEquals(heroes.length,testHeroes.length);
for (int i = 0; i < testHeroes.length;++i)
assertEquals(heroes[i],testHeroes[i]);
}
@Test
public void testFindHeroes() {
// Invoke
Hero[] heroes = heroFileDAO.findHeroes("la");
// Analyze
assertEquals(heroes.length,2);
assertEquals(heroes[0],testHeroes[1]);
assertEquals(heroes[1],testHeroes[2]);
}
@Test
public void testGetHero() {
// Invoke
Hero hero = heroFileDAO.getHero(99);
// Analzye
assertEquals(hero,testHeroes[0]);
}
@Test
public void testDeleteHero() {
// Invoke
boolean result = assertDoesNotThrow(() -> heroFileDAO.deleteHero(99),
"Unexpected exception thrown");
// Analzye
assertEquals(result,true);
// We check the internal tree map size against the length
// of the test heroes array - 1 (because of the delete)
// Because heroes attribute of HeroFileDAO is package private
// we can access it directly
assertEquals(heroFileDAO.heroes.size(),testHeroes.length-1);
}
@Test
public void testCreateHero() {
// Setup
Hero hero = new Hero(102,"Wonder-Person");
// Invoke
Hero result = assertDoesNotThrow(() -> heroFileDAO.createHero(hero),
"Unexpected exception thrown");
// Analyze
assertNotNull(result);
Hero actual = heroFileDAO.getHero(hero.getId());
assertEquals(actual.getId(),hero.getId());
assertEquals(actual.getName(),hero.getName());
}
@Test
public void testUpdateHero() {
// Setup
Hero hero = new Hero(99,"Galactic Agent");
// Invoke
Hero result = assertDoesNotThrow(() -> heroFileDAO.updateHero(hero),
"Unexpected exception thrown");
// Analyze
assertNotNull(result);
Hero actual = heroFileDAO.getHero(hero.getId());
assertEquals(actual,hero);
}
@Test
public void testSaveException() throws IOException{
doThrow(new IOException())
.when(mockObjectMapper)
.writeValue(any(File.class),any(Hero[].class));
Hero hero = new Hero(102,"Wi-Fire");
assertThrows(IOException.class,
() -> heroFileDAO.createHero(hero),
"IOException not thrown");
}
@Test
public void testGetHeroNotFound() {
// Invoke
Hero hero = heroFileDAO.getHero(98);
// Analyze
assertEquals(hero,null);
}
@Test
public void testDeleteHeroNotFound() {
// Invoke
boolean result = assertDoesNotThrow(() -> heroFileDAO.deleteHero(98),
"Unexpected exception thrown");
// Analyze
assertEquals(result,false);
assertEquals(heroFileDAO.heroes.size(),testHeroes.length);
}
@Test
public void testUpdateHeroNotFound() {
// Setup
Hero hero = new Hero(98,"Bolt");
// Invoke
Hero result = assertDoesNotThrow(() -> heroFileDAO.updateHero(hero),
"Unexpected exception thrown");
// Analyze
assertNull(result);
}
@Test
public void testConstructorException() throws IOException {
// Setup
ObjectMapper mockObjectMapper = mock(ObjectMapper.class);
// We want to simulate with a Mock Object Mapper that an
// exception was raised during JSON object deseerialization
// into Java objects
// When the Mock Object Mapper readValue method is called
// from the HeroFileDAO load method, an IOException is
// raised
doThrow(new IOException())
.when(mockObjectMapper)
.readValue(new File("doesnt_matter.txt"),Hero[].class);
// Invoke & Analyze
assertThrows(IOException.class,
() -> new HeroFileDAO("doesnt_matter.txt",mockObjectMapper),
"IOException not thrown");
}
}

View file

@ -0,0 +1,2 @@
server.error.include-message=always
heroes.file=data/heroes.json

View file

@ -0,0 +1,6 @@
com/heroes/api/heroesapi/persistence/HeroFileDAO.class
com/heroes/api/heroesapi/WebConfig.class
com/heroes/api/heroesapi/HeroesApiApplication.class
com/heroes/api/heroesapi/persistence/HeroDAO.class
com/heroes/api/heroesapi/controller/HeroController.class
com/heroes/api/heroesapi/model/Hero.class

View file

@ -0,0 +1,6 @@
/home/icestar/Documents/2231-notes/swen-261-02/coursework/heroesRestAPIStarter/src/main/java/com/heroes/api/heroesapi/controller/HeroController.java
/home/icestar/Documents/2231-notes/swen-261-02/coursework/heroesRestAPIStarter/src/main/java/com/heroes/api/heroesapi/WebConfig.java
/home/icestar/Documents/2231-notes/swen-261-02/coursework/heroesRestAPIStarter/src/main/java/com/heroes/api/heroesapi/persistence/HeroDAO.java
/home/icestar/Documents/2231-notes/swen-261-02/coursework/heroesRestAPIStarter/src/main/java/com/heroes/api/heroesapi/model/Hero.java
/home/icestar/Documents/2231-notes/swen-261-02/coursework/heroesRestAPIStarter/src/main/java/com/heroes/api/heroesapi/persistence/HeroFileDAO.java
/home/icestar/Documents/2231-notes/swen-261-02/coursework/heroesRestAPIStarter/src/main/java/com/heroes/api/heroesapi/HeroesApiApplication.java