Exercise 8 PR #95

Merged
reynisson merged 68 commits from dev into main 2021-11-29 06:46:44 +00:00
33 changed files with 1012 additions and 8 deletions
Showing only changes of commit 5968184b08 - Show all commits

13
app/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# Dockerfile/Docker-Compose file based on an initial version authored by Alexander Lontke (ASSE, Fall Semester 2021)
FROM maven as build
COPY . /app
RUN mvn -f app/pom.xml --batch-mode --update-snapshots verify
FROM openjdk
COPY --from=build /app/target/app-0.1.0.jar ./app-0.1.0.jar
CMD java -jar app-0.1.0.jar

60
docker-compose-local.yml Normal file
View File

@ -0,0 +1,60 @@
# Dockerfile/Docker-Compose file based on an initial version authored by Alexander Lontke (ASSE, Fall Semester 2021)
version: "3.7"
services:
app:
build:
context: ./app
dockerfile: Dockerfile
# Use environment variables instead of application.properties
environment:
- KEY=VALUE
ports: #Just needed when testing from outside the docker network
- "8080:8080"
networks:
- tapas-network
tapas-tasks:
build:
context: ./tapas-tasks
dockerfile: Dockerfile
# Use environment variables instead of application.properties
environment:
- KEY=VALUE
ports: #Just needed when testing from outside
- "8081:8081"
networks:
- tapas-network
tapas-auction-house:
build:
context: ./tapas-auction-house
dockerfile: Dockerfile
# Use environment variables instead of application.properties
environment:
- KEY=VALUE
ports: #Just needed when testing from outside
- "8082:8082"
networks:
- tapas-network
mongodb:
image: mongo
container_name: mongodb
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: 8nP7s0a # Can not be changed again later on
volumes:
- database:/data/db
networks:
- tapas-network
#Volume for mongodb. One per server.
volumes:
database:
networks:
tapas-network:
driver: bridge

View File

@ -14,10 +14,8 @@ Running this WebSub Hub implementation requires Docker, Node.js, and npm:
### How to run
```shell
git clone https://github.com/hemerajs/websub-hub.git
cd websub-hub
docker run -d -p 27017:27017 -p 28017:28017 -e AUTH=no tutum/mongodb
npm i -g websub-hub-cli
docker run -d -p 27017:27017 -p 28017:28017 -e AUTH=no mongo:latest
npm i -g websub-hub
websub-hub -l info -m mongodb://localhost:27017/hub
```

View File

@ -0,0 +1,13 @@
# Dockerfile/Docker-Compose file based on an initial version authored by Alexander Lontke (ASSE, Fall Semester 2021)
FROM maven as build
COPY . /app
RUN mvn -f app/pom.xml --batch-mode --update-snapshots verify
FROM openjdk
COPY --from=build /app/target/tapas-auction-house-0.0.1-SNAPSHOT.jar ./tapas-auction-house-0.0.1-SNAPSHOT.jar
CMD java -jar tapas-auction-house-0.0.1-SNAPSHOT.jar

View File

@ -29,6 +29,7 @@ public class TapasAuctionHouseApplication {
private static ConfigurableEnvironment ENVIRONMENT;
public static void main(String[] args) {
SpringApplication tapasAuctioneerApp = new SpringApplication(TapasAuctionHouseApplication.class);
ENVIRONMENT = tapasAuctioneerApp.run(args).getEnvironment();

View File

@ -1,5 +1,7 @@
server.port=8086
broker.mqtt=tcp://broker.hivemq.com
websub.hub=https://websub.appspot.com/
websub.hub.publish=https://websub.appspot.com/

View File

@ -167,3 +167,25 @@ Date: Sun, 17 Oct 2021 21:32:25 GMT
Internally, this request is mapped to a
[TaskExecutedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java).
The HTTP response returns a `200 OK` status code together with the updated representation of the task.
## Working with MongoDB
The provided TAPAS Tasks service is connected to a MongoDB as a repository for persisting data.
Here are some pointers to start integrating the MongoDB with the other microservices:
* [application.properties](src/main/resources/application.properties) defines the
* URI of the DB server that Spring will connect to (`mongodb`service running in Docker container). Username and password for the server can be found in [docker-compose.yml](../docker-compose.yml).
* Name of the database for the microservice (`tapas-tasks`)
* [docker-compose.yml](../docker-compose.yml) defines
* in lines 74-82: the configuration of the mongodb service based on the mongodb container including the root username and password (once deployed this cannot be changed anymore!)
* in lines 84-102: the configuration of a web application called `mongo-express` to manage the MongoDB server. The web app can be reached via the URI: [http://dbadmin.${PUB_IP}.nip.io]([http://dbadmin.${PUB_IP}.nip.io]). Login credentials for mongo-express can be found in lines 89 and 90.
* in lines 104-105: the volume to be used by the mongodb service for writing and storing data (do not forget!).
* The [pom.xml](./pom.xml) needs to have `spring-boot-starter-data-mongodb` and `spring-data-mongodb` as new dependencies.
* The [TapasTasksApplication.java](/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java) specifies in line 9 the location of the MongoRepository classes for the microservice.
* The [persistence.mongodb](/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/persistence/mongodb) package has the relevant classes to work with MongoDB:
* The [MongoTaskDocument.java](/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/persistence/mongodb/MongoTaskDocument.java) class defines the attributes of a Document for storing a task in the collection `tasks`.
* The [TaskRepository.java](/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/persistence/mongodb/TaskRepository.java) class specifies the MongoRepository.
* The [TaskPersistenceAdapter.java](/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/persistence/mongodb/TaskPersistenceAdapter.java) implements the two ports to add a new task ([AddTaskPort](/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/AddTaskPort.java)) and retrieve a task ([LoadTaskPort](/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/LoadTaskPort.java)). These ports are used in the classes [AddNewTaskToTaskListService.java](/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java) and [RetrieveTaskFromTaskListService.java](/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java).
#### General hints:
* To not overload the VMs we recommend to use only one MongoDB server that all microservices connect to. Per microservice you could use one database or one collection (discuss in your ADRs!). To use more than one MongoDB server you have to extend the [docker-compose.yml](../docker-compose.yml) file by basically replicating lines 74-105 and changing the names of the services and volumes to be unique (ask your tutors!).
* For local testing you have to install the MongoDB server locally on your computers and change the `spring.data.mongodb.uri` String in [application.properties](./src/main/resources/application.properties).

View File

@ -29,6 +29,25 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>3.2.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>chaos-monkey-spring-boot</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@ -40,6 +59,17 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.21.0</version>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>0.22.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -84,7 +114,10 @@
<version>0.0.20131108.vaadin1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -1,14 +1,20 @@
package ch.unisg.tapastasks;
import ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb.TaskRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
@SpringBootApplication
@EnableMongoRepositories(basePackageClasses = TaskRepository.class)
public class TapasTasksApplication {
public static void main(String[] args) {
SpringApplication tapasTasksApp = new SpringApplication(TapasTasksApplication.class);
tapasTasksApp.run(args);
}
}

View File

@ -0,0 +1,32 @@
package ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Document(collection = "tasks")
public class MongoTaskDocument {
@Id
public String taskId;
public String taskName;
public String taskType;
public String originalTaskUri;
public String taskStatus;
public String taskListName;
public MongoTaskDocument(String taskId, String taskName, String taskType,
String originalTaskUri,
String taskStatus, String taskListName) {
this.taskId = taskId;
this.taskName = taskName;
this.taskType = taskType;
this.originalTaskUri = originalTaskUri;
this.taskStatus = taskStatus;
this.taskListName = taskListName;
}
}

View File

@ -0,0 +1,30 @@
package ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import org.springframework.stereotype.Component;
@Component
class TaskMapper {
Task mapToDomainEntity(MongoTaskDocument task) {
return Task.withIdNameTypeOriginaluriStatus(
new Task.TaskId(task.taskId),
new Task.TaskName(task.taskName),
new Task.TaskType(task.taskType),
new Task.OriginalTaskUri(task.originalTaskUri),
new Task.TaskStatus(Task.Status.valueOf(task.taskStatus))
);
}
MongoTaskDocument mapToMongoDocument(Task task) {
return new MongoTaskDocument(
task.getTaskId().getValue(),
task.getTaskName().getValue(),
task.getTaskType().getValue(),
task.getOriginalTaskUri().getValue(),
task.getTaskStatus().getValue().toString(),
TaskList.getTapasTaskList().getTaskListName().getValue()
);
}
}

View File

@ -0,0 +1,34 @@
package ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb;
import ch.unisg.tapastasks.tasks.application.port.out.AddTaskPort;
import ch.unisg.tapastasks.tasks.application.port.out.LoadTaskPort;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class TaskPersistenceAdapter implements
AddTaskPort,
LoadTaskPort {
@Autowired
private final TaskRepository taskRepository;
private final TaskMapper taskMapper;
@Override
public void addTask(Task task) {
MongoTaskDocument mongoTaskDocument = taskMapper.mapToMongoDocument(task);
taskRepository.save(mongoTaskDocument);
}
@Override
public Task loadTask(Task.TaskId taskId, TaskList.TaskListName taskListName) {
MongoTaskDocument mongoTaskDocument = taskRepository.findByTaskId(taskId.getValue(),taskListName.getValue());
Task task = taskMapper.mapToDomainEntity(mongoTaskDocument);
return task;
}
}

View File

@ -0,0 +1,14 @@
package ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TaskRepository extends MongoRepository<MongoTaskDocument,String> {
public MongoTaskDocument findByTaskId(String taskId, String taskListName);
public List<MongoTaskDocument> findByTaskListName(String taskListName);
}

View File

@ -0,0 +1,9 @@
package ch.unisg.tapastasks.tasks.application.port.out;
import ch.unisg.tapastasks.tasks.domain.Task;
public interface AddTaskPort {
void addTask(Task task);
}

View File

@ -0,0 +1,10 @@
package ch.unisg.tapastasks.tasks.application.port.out;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
public interface LoadTaskPort {
Task loadTask(Task.TaskId taskId, TaskList.TaskListName taskListName);
}

View File

@ -0,0 +1,11 @@
package ch.unisg.tapastasks.tasks.application.port.out;
import ch.unisg.tapastasks.tasks.domain.TaskList;
public interface TaskListLock {
void lockTaskList(TaskList.TaskListName taskListName);
void releaseTaskList(TaskList.TaskListName taskListName);
}

View File

@ -2,26 +2,35 @@ package ch.unisg.tapastasks.tasks.application.service;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase;
import ch.unisg.tapastasks.tasks.application.port.out.AddTaskPort;
import ch.unisg.tapastasks.tasks.application.port.out.NewTaskAddedEventPort;
import ch.unisg.tapastasks.tasks.application.port.out.TaskListLock;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.NewTaskAddedEvent;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@RequiredArgsConstructor
@Component
@Transactional
@Service("AddNewTaskToTaskList")
public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase {
private final NewTaskAddedEventPort newTaskAddedEventPort;
private final AddTaskPort addTaskToRepositoryPort;
private final TaskListLock taskListLock;
@Override
public Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command) {
TaskList taskList = TaskList.getTapasTaskList();
taskListLock.lockTaskList(taskList.getTaskListName());
Task newTask;
if (command.getOriginalTaskUri().isPresent() && command.getInputData().isPresent()) {
@ -37,6 +46,9 @@ public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase
newTask = taskList.addNewTaskWithNameAndType(command.getTaskName(), command.getTaskType());
}
addTaskToRepositoryPort.addTask(newTask);
taskListLock.releaseTaskList(taskList.getTaskListName());
//Here we are using the application service to emit the domain event to the outside of the bounded context.
//This event should be considered as a light-weight "integration event" to communicate with other services.
//Domain events are usually rather "fat". In our implementation we simplify at this point. In general, it is

View File

@ -0,0 +1,18 @@
package ch.unisg.tapastasks.tasks.application.service;
import ch.unisg.tapastasks.tasks.application.port.out.TaskListLock;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import org.springframework.stereotype.Component;
@Component
public class NoOpTaskListLock implements TaskListLock {
@Override
public void lockTaskList(TaskList.TaskListName taskListName) {
//do nothing
}
@Override
public void releaseTaskList(TaskList.TaskListName taskListName) {
//do nothing
}
}

View File

@ -2,10 +2,12 @@ package ch.unisg.tapastasks.tasks.application.service;
import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListQuery;
import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListUseCase;
import ch.unisg.tapastasks.tasks.application.port.out.LoadTaskPort;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.Optional;
@ -13,10 +15,19 @@ import java.util.Optional;
@RequiredArgsConstructor
@Component
@Transactional
@Service("RetrieveTaskFromTaskList")
public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase {
private final LoadTaskPort loadTaskFromRepositoryPort;
@Override
public Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query) {
TaskList taskList = TaskList.getTapasTaskList();
return taskList.retrieveTaskById(query.getTaskId());
Optional<Task> task = taskList.retrieveTaskById(query.getTaskId());
Optional<Task> taskFromRepo = Optional.ofNullable(loadTaskFromRepositoryPort.loadTask(query.getTaskId(), taskList.getTaskListName()));
return taskFromRepo;
}
}

View File

@ -80,11 +80,22 @@ public class Task {
this.outputData = null;
}
public Task(TaskId taskId, TaskName taskName, TaskType taskType, OriginalTaskUri taskUri,
TaskStatus taskStatus) {
this.taskId = taskId;
this.taskName = taskName;
this.taskType = taskType;
this.originalTaskUri = taskUri;
this.taskStatus = taskStatus;
this.inputData = null;
this.outputData = null;
}
protected static Task createTaskWithNameAndType(TaskName name, TaskType type) {
return new Task(name, type);
}
protected static Task createTaskWithNameAndTypeAndOriginalTaskUri(TaskName name, TaskType type,
public static Task createTaskWithNameAndTypeAndOriginalTaskUri(TaskName name, TaskType type,
OriginalTaskUri originalTaskUri) {
return new Task(name, type, originalTaskUri);
}
@ -99,6 +110,13 @@ public class Task {
return new Task(name, type, originalTaskUri, inputData);
}
public static Task withIdNameTypeOriginaluriStatus(TaskId taskId, TaskName taskName,
TaskType taskType,
OriginalTaskUri originalTaskUri,
TaskStatus taskStatus) {
return new Task(taskId, taskName, taskType, originalTaskUri, taskStatus);
}
@Value
public static class TaskId {
String value;

View File

@ -1,3 +1,40 @@
server.port=8081
spring.data.mongodb.uri=mongodb://127.0.0.1:27017
#spring.data.mongodb.uri=mongodb://root:8nP7s0a@mongodb:27017/
spring.data.mongodb.database=tapas-tasks
baseuri=https://tapas-tasks.86-119-34-23.nip.io/
roster.uri=http://127.0.0.1:8082
spring.profiles.active=chaos-monkey
chaos.monkey.enabled=true
management.endpoint.chaosmonkey.enabled=true
management.endpoint.chaosmonkeyjmx.enabled=true
# include specific endpoints
management.endpoints.web.exposure.include=health,info,chaosmonkey
chaos.monkey.watcher.controller=true
chaos.monkey.watcher.restController=true
chaos.monkey.watcher.service=true
chaos.monkey.watcher.repository=true
chaos.monkey.watcher.component=true
#Chaos Monkey configs taken from here: https://www.baeldung.com/spring-boot-chaos-monkey
#Latency Assault
#chaos.monkey.assaults.latencyActive=true
#chaos.monkey.assaults.latencyRangeStart=3000
#chaos.monkey.assaults.latencyRangeEnd=15000
#Exception Assault
#chaos.monkey.assaults.latencyActive=false
#chaos.monkey.assaults.exceptionsActive=true
#chaos.monkey.assaults.killApplicationActive=false
#AppKiller Assault
#chaos.monkey.assaults.latencyActive=false
#chaos.monkey.assaults.exceptionsActive=false
#chaos.monkey.assaults.killApplicationActive=true
#Chaos Monkey assaults via REST to endpoint /actuator/chaosmonkey/assaults/
#https://softwarehut.com/blog/tech/chaos-monkey
#https://codecentric.github.io/chaos-monkey-spring-boot/latest/

View File

@ -0,0 +1,76 @@
package ch.unisg.tapastasks;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
import ch.unisg.tapastasks.tasks.application.port.out.AddTaskPort;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import static org.assertj.core.api.BDDAssertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AddNewTaskToTaskListSystemTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private AddTaskPort addTaskPort;
@Test
void addNewTaskToTaskList() throws JSONException {
Task.TaskName taskName = new Task.TaskName("system-integration-test-task");
Task.TaskType taskType = new Task.TaskType("system-integration-test-type");
Task.OriginalTaskUri originalTaskUri = new Task.OriginalTaskUri("example.org");
ResponseEntity response = whenAddNewTaskToEmptyList(taskName, taskType, originalTaskUri);
JSONObject responseJson = new JSONObject(response.getBody().toString());
String respTaskId = responseJson.getString("taskId");
String respTaskName = responseJson.getString("taskName");
String respTaskType = responseJson.getString("taskType");
then(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
then(respTaskId).isNotEmpty();
then(respTaskName).isEqualTo(taskName.getValue());
then(respTaskType).isEqualTo(taskType.getValue());
then(TaskList.getTapasTaskList().getListOfTasks().getValue()).hasSize(1);
}
private ResponseEntity whenAddNewTaskToEmptyList(
Task.TaskName taskName,
Task.TaskType taskType,
Task.OriginalTaskUri originalTaskUri) throws JSONException {
TaskList.getTapasTaskList().getListOfTasks().getValue().clear();
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", TaskJsonRepresentation.MEDIA_TYPE);
String jsonPayLoad = new JSONObject()
.put("taskName", taskName.getValue() )
.put("taskType", taskType.getValue())
.put("originalTaskUri",originalTaskUri.getValue())
.toString();
HttpEntity<String> request = new HttpEntity<>(jsonPayLoad,headers);
return restTemplate.exchange(
"/tasks/",
HttpMethod.POST,
request,
Object.class
);
}
}

View File

@ -0,0 +1,24 @@
package ch.unisg.tapastasks;
import ch.unisg.tapastasks.archunit.HexagonalArchitecture;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
class DependencyRuleTests {
@Test
void testPackageDependencies() {
noClasses()
.that()
.resideInAPackage("ch.unisg.tapastasks.tasks.domain..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("ch.unisg.tapastasks.tasks.application..")
.check(new ClassFileImporter()
.importPackages("ch.unisg.tapastasks.tasks.."));
}
}

View File

@ -1,5 +1,9 @@
package ch.unisg.tapastasks;
import ch.unisg.tapastasks.tasks.application.port.out.AddTaskPort;
import ch.unisg.tapastasks.tasks.application.port.out.NewTaskAddedEventPort;
import ch.unisg.tapastasks.tasks.application.port.out.TaskListLock;
import ch.unisg.tapastasks.tasks.application.service.AddNewTaskToTaskListService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@ -8,6 +12,7 @@ class TapasTasksApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -0,0 +1,62 @@
package ch.unisg.tapastasks.archunit;
import com.tngtech.archunit.core.domain.JavaClasses;
import java.util.ArrayList;
import java.util.List;
public class Adapters extends ArchitectureElement {
private final HexagonalArchitecture parentContext;
private List<String> incomingAdapterPackages = new ArrayList<>();
private List<String> outgoingAdapterPackages = new ArrayList<>();
Adapters(HexagonalArchitecture parentContext, String basePackage) {
super(basePackage);
this.parentContext = parentContext;
}
public Adapters outgoing(String packageName) {
this.incomingAdapterPackages.add(fullQualifiedPackage(packageName));
return this;
}
public Adapters incoming(String packageName) {
this.outgoingAdapterPackages.add(fullQualifiedPackage(packageName));
return this;
}
List<String> allAdapterPackages() {
List<String> allAdapters = new ArrayList<>();
allAdapters.addAll(incomingAdapterPackages);
allAdapters.addAll(outgoingAdapterPackages);
return allAdapters;
}
public HexagonalArchitecture and() {
return parentContext;
}
String getBasePackage() {
return basePackage;
}
void dontDependOnEachOther(JavaClasses classes) {
List<String> allAdapters = allAdapterPackages();
for (String adapter1 : allAdapters) {
for (String adapter2 : allAdapters) {
if (!adapter1.equals(adapter2)) {
denyDependency(adapter1, adapter2, classes);
}
}
}
}
void doesNotDependOn(String packageName, JavaClasses classes) {
denyDependency(this.basePackage, packageName, classes);
}
void doesNotContainEmptyPackages() {
denyEmptyPackages(allAdapterPackages());
}
}

View File

@ -0,0 +1,59 @@
package ch.unisg.tapastasks.archunit;
import com.tngtech.archunit.core.domain.JavaClasses;
import java.util.ArrayList;
import java.util.List;
public class ApplicationLayer extends ArchitectureElement {
private final HexagonalArchitecture parentContext;
private List<String> incomingPortsPackages = new ArrayList<>();
private List<String> outgoingPortsPackages = new ArrayList<>();
private List<String> servicePackages = new ArrayList<>();
public ApplicationLayer(String basePackage, HexagonalArchitecture parentContext) {
super(basePackage);
this.parentContext = parentContext;
}
public ApplicationLayer incomingPorts(String packageName) {
this.incomingPortsPackages.add(fullQualifiedPackage(packageName));
return this;
}
public ApplicationLayer outgoingPorts(String packageName) {
this.outgoingPortsPackages.add(fullQualifiedPackage(packageName));
return this;
}
public ApplicationLayer services(String packageName) {
this.servicePackages.add(fullQualifiedPackage(packageName));
return this;
}
public HexagonalArchitecture and() {
return parentContext;
}
public void doesNotDependOn(String packageName, JavaClasses classes) {
denyDependency(this.basePackage, packageName, classes);
}
public void incomingAndOutgoingPortsDoNotDependOnEachOther(JavaClasses classes) {
denyAnyDependency(this.incomingPortsPackages, this.outgoingPortsPackages, classes);
denyAnyDependency(this.outgoingPortsPackages, this.incomingPortsPackages, classes);
}
private List<String> allPackages() {
List<String> allPackages = new ArrayList<>();
allPackages.addAll(incomingPortsPackages);
allPackages.addAll(outgoingPortsPackages);
allPackages.addAll(servicePackages);
return allPackages;
}
void doesNotContainEmptyPackages() {
denyEmptyPackages(allPackages());
}
}

View File

@ -0,0 +1,74 @@
package ch.unisg.tapastasks.archunit;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import java.util.List;
import static com.tngtech.archunit.base.DescribedPredicate.*;
import static com.tngtech.archunit.base.DescribedPredicate.greaterThanOrEqualTo;
import static com.tngtech.archunit.lang.conditions.ArchConditions.*;
import static com.tngtech.archunit.lang.conditions.ArchConditions.containNumberOfElements;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
abstract class ArchitectureElement {
final String basePackage;
public ArchitectureElement(String basePackage) {
this.basePackage = basePackage;
}
String fullQualifiedPackage(String relativePackage) {
return this.basePackage + "." + relativePackage;
}
static void denyDependency(String fromPackageName, String toPackageName, JavaClasses classes) {
noClasses()
.that()
.resideInAPackage("ch.unisg.tapastasks.tasks.domain..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("ch.unisg.tapastasks.tasks.application..")
.check(classes);
}
static void denyAnyDependency(
List<String> fromPackages, List<String> toPackages, JavaClasses classes) {
for (String fromPackage : fromPackages) {
for (String toPackage : toPackages) {
noClasses()
.that()
.resideInAPackage(matchAllClassesInPackage(fromPackage))
.should()
.dependOnClassesThat()
.resideInAnyPackage(matchAllClassesInPackage(toPackage))
.check(classes);
}
}
}
static String matchAllClassesInPackage(String packageName) {
return packageName + "..";
}
void denyEmptyPackage(String packageName) {
classes()
.that()
.resideInAPackage(matchAllClassesInPackage(packageName))
.should(containNumberOfElements(greaterThanOrEqualTo(1)))
.check(classesInPackage(packageName));
}
private JavaClasses classesInPackage(String packageName) {
return new ClassFileImporter().importPackages(packageName);
}
void denyEmptyPackages(List<String> packages) {
for (String packageName : packages) {
denyEmptyPackage(packageName);
}
}
}

View File

@ -0,0 +1,61 @@
package ch.unisg.tapastasks.archunit;
import com.tngtech.archunit.core.domain.JavaClasses;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HexagonalArchitecture extends ArchitectureElement {
private Adapters adapters;
private ApplicationLayer applicationLayer;
private String configurationPackage;
private List<String> domainPackages = new ArrayList<>();
public static HexagonalArchitecture boundedContext(String basePackage) {
return new HexagonalArchitecture(basePackage);
}
public HexagonalArchitecture(String basePackage) {
super(basePackage);
}
public Adapters withAdaptersLayer(String adaptersPackage) {
this.adapters = new Adapters(this, fullQualifiedPackage(adaptersPackage));
return this.adapters;
}
public HexagonalArchitecture withDomainLayer(String domainPackage) {
this.domainPackages.add(fullQualifiedPackage(domainPackage));
return this;
}
public ApplicationLayer withApplicationLayer(String applicationPackage) {
this.applicationLayer = new ApplicationLayer(fullQualifiedPackage(applicationPackage), this);
return this.applicationLayer;
}
public HexagonalArchitecture withConfiguration(String packageName) {
this.configurationPackage = fullQualifiedPackage(packageName);
return this;
}
private void domainDoesNotDependOnOtherPackages(JavaClasses classes) {
denyAnyDependency(
this.domainPackages, Collections.singletonList(adapters.basePackage), classes);
denyAnyDependency(
this.domainPackages, Collections.singletonList(applicationLayer.basePackage), classes);
}
public void check(JavaClasses classes) {
this.adapters.doesNotContainEmptyPackages();
this.adapters.dontDependOnEachOther(classes);
this.adapters.doesNotDependOn(this.configurationPackage, classes);
this.applicationLayer.doesNotContainEmptyPackages();
this.applicationLayer.doesNotDependOn(this.adapters.getBasePackage(), classes);
this.applicationLayer.doesNotDependOn(this.configurationPackage, classes);
this.applicationLayer.incomingAndOutgoingPortsDoNotDependOnEachOther(classes);
this.domainDoesNotDependOnOtherPackages(classes);
}
}

View File

@ -0,0 +1,68 @@
package ch.unisg.tapastasks.tasks.adapter.in.web;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
import ch.unisg.tapastasks.tasks.adapter.in.web.AddNewTaskToTaskListWebController;
import ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb.TaskRepository;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase;
import ch.unisg.tapastasks.tasks.domain.Task;
import org.json.JSONObject;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
import static org.mockito.BDDMockito.eq;
import static org.mockito.BDDMockito.then;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(controllers = AddNewTaskToTaskListWebController.class)
public class AddNewTaskToTaskListWebControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase;
@MockBean
TaskRepository taskRepository;
@Disabled
@Test
void testAddNewTaskToTaskList() throws Exception {
String taskName = "test-request";
String taskType = "test-request-type";
String originalTaskUri = "example.org";
String jsonPayLoad = new JSONObject()
.put("taskName", taskName )
.put("taskType", taskType)
.put("originalTaskUri",originalTaskUri)
.toString();
//This raises a NullPointerException since it tries to build the HTTP response with attributes from
//the domain object (created task), which is mocked --> we need System Tests here!
//See the buckpal example from the lecture for a working integration test for testing the web controller
mockMvc.perform(post("/tasks/")
.contentType(TaskJsonRepresentation.MEDIA_TYPE)
.content(jsonPayLoad))
.andExpect(status().isCreated());
then(addNewTaskToTaskListUseCase).should()
.addNewTaskToTaskList(eq(new AddNewTaskToTaskListCommand(
new Task.TaskName(taskName), new Task.TaskType(taskType),
Optional.of(new Task.OriginalTaskUri(originalTaskUri))
)));
}
}

View File

@ -0,0 +1,77 @@
package ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.AutoConfigureDataMongo;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureDataMongo
@Import({TaskPersistenceAdapter.class, TaskMapper.class})
public class TaskPersistenceAdapterTest {
@Autowired
private TaskRepository taskRepository;
@Autowired
private TaskPersistenceAdapter adapterUnderTest;
@Test
void addsNewTask() {
String testTaskId = UUID.randomUUID().toString();
String testTaskName = "adds-persistence-task-name";
String testTaskType = "adds-persistence-task-type";
String testTaskOuri = "adds-persistence-test-task-ouri";
String testTaskStatus = Task.Status.OPEN.toString();
String testTaskListName = "tapas-tasks-tutors";
Task testTask = new Task(
new Task.TaskId(testTaskId),
new Task.TaskName(testTaskName),
new Task.TaskType(testTaskType),
new Task.OriginalTaskUri(testTaskOuri),
new Task.TaskStatus(Task.Status.valueOf(testTaskStatus))
);
adapterUnderTest.addTask(testTask);
MongoTaskDocument retrievedDoc = taskRepository.findByTaskId(testTaskId,testTaskListName);
assertThat(retrievedDoc.taskId).isEqualTo(testTaskId);
assertThat(retrievedDoc.taskName).isEqualTo(testTaskName);
assertThat(retrievedDoc.taskListName).isEqualTo(testTaskListName);
}
@Test
void retrievesTask() {
String testTaskId = UUID.randomUUID().toString();
String testTaskName = "reads-persistence-task-name";
String testTaskType = "reads-persistence-task-type";
String testTaskOuri = "reads-persistence-test-task-ouri";
String testTaskStatus = Task.Status.OPEN.toString();
String testTaskListName = "tapas-tasks-tutors";
MongoTaskDocument mongoTask = new MongoTaskDocument(testTaskId, testTaskName, testTaskType, testTaskOuri,
testTaskStatus, testTaskListName);
taskRepository.insert(mongoTask);
Task retrievedTask = adapterUnderTest.loadTask(new Task.TaskId(testTaskId),
new TaskList.TaskListName(testTaskListName));
assertThat(retrievedTask.getTaskName().getValue()).isEqualTo(testTaskName);
assertThat(retrievedTask.getTaskId().getValue()).isEqualTo(testTaskId);
assertThat(retrievedTask.getTaskStatus().getValue()).isEqualTo(Task.Status.valueOf(testTaskStatus));
}
}

View File

@ -0,0 +1,63 @@
package ch.unisg.tapastasks.tasks.application.service;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand;
import ch.unisg.tapastasks.tasks.application.port.out.AddTaskPort;
import ch.unisg.tapastasks.tasks.application.port.out.NewTaskAddedEventPort;
import ch.unisg.tapastasks.tasks.application.port.out.TaskListLock;
import ch.unisg.tapastasks.tasks.domain.NewTaskAddedEvent;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Optional;
import static org.mockito.BDDMockito.*;
import static org.assertj.core.api.Assertions.*;
public class AddNewTaskToTaskListServiceTest {
private final AddTaskPort addTaskPort = Mockito.mock(AddTaskPort.class);
private final TaskListLock taskListLock = Mockito.mock(TaskListLock.class);
private final NewTaskAddedEventPort newTaskAddedEventPort = Mockito.mock(NewTaskAddedEventPort.class);
private final AddNewTaskToTaskListService addNewTaskToTaskListService = new AddNewTaskToTaskListService(
newTaskAddedEventPort, addTaskPort, taskListLock);
@Test
void addingSucceeds() {
Task newTask = givenATaskWithNameAndTypeAndURI(new Task.TaskName("test-task"),
new Task.TaskType("test-type"), Optional.of(new Task.OriginalTaskUri("example.org")));
TaskList taskList = givenAnEmptyTaskList(TaskList.getTapasTaskList());
AddNewTaskToTaskListCommand addNewTaskToTaskListCommand = new AddNewTaskToTaskListCommand(newTask.getTaskName(),
newTask.getTaskType(), Optional.ofNullable(newTask.getOriginalTaskUri()));
Task addedTask = addNewTaskToTaskListService.addNewTaskToTaskList(addNewTaskToTaskListCommand);
assertThat(addedTask).isNotNull();
assertThat(taskList.getListOfTasks().getValue()).hasSize(1);
then(taskListLock).should().lockTaskList(eq(TaskList.getTapasTaskList().getTaskListName()));
then(newTaskAddedEventPort).should(times(1))
.publishNewTaskAddedEvent(any(NewTaskAddedEvent.class));
}
private TaskList givenAnEmptyTaskList(TaskList taskList) {
taskList.getListOfTasks().getValue().clear();
return taskList;
}
private Task givenATaskWithNameAndTypeAndURI(Task.TaskName taskName, Task.TaskType taskType,
Optional<Task.OriginalTaskUri> originalTaskUri) {
Task task = Mockito.mock(Task.class);
given(task.getTaskName()).willReturn(taskName);
given(task.getTaskType()).willReturn(taskType);
given(task.getOriginalTaskUri()).willReturn(originalTaskUri.get());
return task;
}
}

View File

@ -0,0 +1,49 @@
package ch.unisg.tapastasks.tasks.domain;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
public class TaskListTest {
@Test
void addNewTaskToTaskListSuccess() {
TaskList taskList = TaskList.getTapasTaskList();
taskList.getListOfTasks().getValue().clear();
Task newTask = taskList.addNewTaskWithNameAndType(new Task.TaskName("My-Test-Task"),
new Task.TaskType("My-Test-Type"));
assertThat(newTask.getTaskName().getValue()).isEqualTo("My-Test-Task");
assertThat(taskList.getListOfTasks().getValue()).hasSize(1);
assertThat(taskList.getListOfTasks().getValue().get(0)).isEqualTo(newTask);
}
@Test
void retrieveTaskSuccess() {
TaskList taskList = TaskList.getTapasTaskList();
Task newTask = taskList.addNewTaskWithNameAndType(new Task.TaskName("My-Test-Task2"),
new Task.TaskType("My-Test-Type2"));
Task retrievedTask = taskList.retrieveTaskById(newTask.getTaskId()).get();
assertThat(retrievedTask).isEqualTo(newTask);
}
@Test
void retrieveTaskFailure() {
TaskList taskList = TaskList.getTapasTaskList();
Task newTask = taskList.addNewTaskWithNameAndType(new Task.TaskName("My-Test-Task3"),
new Task.TaskType("My-Test-Type3"));
Task.TaskId fakeId = new Task.TaskId("fake-id");
Optional<Task> retrievedTask = taskList.retrieveTaskById(fakeId);
assertThat(retrievedTask.isPresent()).isFalse();
}
}

View File

@ -0,0 +1,2 @@
spring.data.mongodb.uri=mongodb://127.0.0.1:27017
spring.data.mongodb.database=tapas-tasks