Skip to content

Commit

Permalink
Merge pull request #73 from tillias/dev
Browse files Browse the repository at this point in the history
Duplicated dependencies validation #71 and bugfixing #68
  • Loading branch information
tillias authored Oct 28, 2020
2 parents 03f9cc8 + d26293f commit acaf233
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.github.microcatalog.domain.Microservice;
import com.github.microcatalog.repository.DependencyRepository;
import com.github.microcatalog.service.custom.exceptions.CircularDependenciesException;
import com.github.microcatalog.service.custom.exceptions.DuplicateDependencyException;
import com.github.microcatalog.service.custom.exceptions.SelfCircularException;
import org.jgrapht.Graph;
import org.jgrapht.alg.cycle.CycleDetector;
Expand Down Expand Up @@ -83,6 +84,9 @@ private void validateSelfCycle(final Dependency dependency) {

private void validateIfAdded(final Dependency toBeAdded) {
final Graph<Microservice, DefaultEdge> graph = graphLoaderService.loadGraph();

checkDuplicateWillBeIntroduced(graph,toBeAdded);

graph.addEdge(toBeAdded.getSource(), toBeAdded.getTarget());

checkCycles(graph);
Expand All @@ -98,11 +102,20 @@ private void validateIfUpdated(final Dependency dependency) {
final DefaultEdge currentEdge = graph.getEdge(persistent.getSource(), persistent.getTarget());
graph.removeEdge(currentEdge);

checkDuplicateWillBeIntroduced(graph,dependency);

graph.addEdge(dependency.getSource(), dependency.getTarget());

checkCycles(graph);
}

private void checkDuplicateWillBeIntroduced(final Graph<Microservice, DefaultEdge> graph, final Dependency dependency){
final DefaultEdge existingEdge = graph.getEdge(dependency.getSource(), dependency.getTarget());
if (existingEdge != null) {
throw new DuplicateDependencyException("Dependency already exists", dependency);
}
}

private void checkCycles(final Graph<Microservice, DefaultEdge> graph) {
final CycleDetector<Microservice, DefaultEdge> cycleDetector = new CycleDetector<>(graph);
if (cycleDetector.detectCycles()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.github.microcatalog.service.custom.exceptions;

import com.github.microcatalog.domain.Dependency;

public class DuplicateDependencyException extends RuntimeException {
private final Dependency dependency;

public DuplicateDependencyException(String message, Dependency dependency) {
super(message);
this.dependency = dependency;
}

public Dependency getDependency() {
return dependency;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

public class DependencyBuilder {
private Long id;
private String name;
private Long source;
private Long target;

Expand All @@ -12,6 +13,11 @@ public DependencyBuilder withId(Long id) {
return this;
}

public DependencyBuilder withName(String name) {
this.name = name;
return this;
}

public DependencyBuilder withSource(Long sourceId) {
this.source = sourceId;
return this;
Expand All @@ -24,7 +30,7 @@ public DependencyBuilder withTarget(Long targetId) {

public Dependency build() {
final Dependency result = new Dependency();

result.setName(name);
result.setId(id);
result.setSource(new MicroserviceBuilder().withId(source).build());
result.setTarget(new MicroserviceBuilder().withId(target).build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@

public class MicroserviceBuilder {
private Long id;
private String name;

public MicroserviceBuilder withId(Long id) {
this.id = id;
return this;
}

public MicroserviceBuilder withName(String name){
this.name = name;
return this;
}

public Microservice build() {
final Microservice result = new Microservice();
result.setId(id);
result.setName(name);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.github.microcatalog.web.rest.errors.custom;

import com.github.microcatalog.domain.Dependency;
import com.github.microcatalog.domain.Microservice;
import com.github.microcatalog.service.custom.exceptions.CircularDependenciesException;
import com.github.microcatalog.service.custom.exceptions.DuplicateDependencyException;
import com.github.microcatalog.service.custom.exceptions.SelfCircularException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand All @@ -19,32 +21,47 @@
*/
public interface ReleasePathAdviceTrait extends AdviceTrait {

String HEADER_KEY = "BusinessException";
String PAYLOAD_KEY = "BusinessPayload";
String EXCEPTION_KEY = "BusinessException";
String MESSAGE_KEY = "BusinessMessage";

@ExceptionHandler
default ResponseEntity<Problem> handleDuplicateDependencyException(final DuplicateDependencyException exception, final NativeWebRequest request) {
final Dependency dependency = exception.getDependency();

final Problem problem = Problem.builder()
.withStatus(UNPROCESSABLE_ENTITY)
.with(EXCEPTION_KEY, DuplicateDependencyException.class.getSimpleName())
.with(MESSAGE_KEY, exception.getMessage())
.with("dependencyId", String.valueOf(dependency.getId()))
.with("dependencyName", dependency.getName())
.build();

return create(exception, problem, request);

}

@ExceptionHandler
default ResponseEntity<Problem> handleSelfCircularException(final SelfCircularException exception, final NativeWebRequest request) {
final Microservice source = exception.getSource();
final Problem problem = Problem.builder()
.withStatus(UNPROCESSABLE_ENTITY)
.with(HEADER_KEY, SelfCircularException.class.getName())
.with(PAYLOAD_KEY, String.format("id = %s, name = %s", source.getId(), source.getName()))
.with(EXCEPTION_KEY, SelfCircularException.class.getSimpleName())
.with(MESSAGE_KEY, exception.getMessage())
.with("microserviceName", source.getName())
.build();

return create(exception, problem, request);
}

@ExceptionHandler
default ResponseEntity<Problem> handleCircularDependenciesException(final CircularDependenciesException exception, final NativeWebRequest request) {
final List<String> microserviceIds = exception.getCycles().stream().map(m -> String.valueOf(m.getId())).collect(Collectors.toList());
final List<String> microservices = exception.getCycles().stream().map(Microservice::getName).collect(Collectors.toList());

final Problem problem = Problem.builder()
.withStatus(UNPROCESSABLE_ENTITY)
.with(HEADER_KEY, CircularDependenciesException.class.getName())
.with(PAYLOAD_KEY, String.join(",", microserviceIds))
.with(EXCEPTION_KEY, CircularDependenciesException.class.getSimpleName())
.with(MESSAGE_KEY, exception.getMessage())
.with("microservices", String.join(",", microservices))
.build();

return create(exception, problem, request);
Expand Down
2 changes: 1 addition & 1 deletion src/main/webapp/app/shared/alert/alert-error.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class AlertErrorComponent implements OnDestroy {
break;

case 422:
this.addErrorAlert(httpErrorResponse.error.BusinessMessage);
this.addErrorAlert('', 'business.' + httpErrorResponse.error.BusinessException, httpErrorResponse.error);
break;

default:
Expand Down
5 changes: 5 additions & 0 deletions src/main/webapp/i18n/en/error.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"business": {
"DuplicateDependencyException": "Dependency already exists: {{dependencyName}}",
"CircularDependenciesException": "Circular dependency will be introduced. Cycle: {{microservices}}",
"SelfCircularException": "Source of dependency can't be the same as target. Source: {{microserviceName}}"
},
"error": {
"title": "Error page!",
"http": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import com.github.microcatalog.repository.DependencyRepository;
import com.github.microcatalog.service.GraphUtils;
import com.github.microcatalog.service.custom.exceptions.CircularDependenciesException;
import com.github.microcatalog.service.custom.exceptions.DuplicateDependencyException;
import com.github.microcatalog.service.custom.exceptions.SelfCircularException;
import com.github.microcatalog.utils.DependencyBuilder;
import com.github.microcatalog.utils.MicroserviceBuilder;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -103,6 +105,32 @@ void create_WillIntroduceCircularDependencies_ExceptionIsThrown() {
.containsExactlyInAnyOrder(1L, 2L, 3L, 4L);
}

@Test
void create_DuplicateWillBeIntroduced_ExceptionIsThrown() {
given(graphLoaderService.loadGraph()).willReturn(
GraphUtils.createGraph(
String.join("\n",
"strict digraph G { ",
"1; 2; 3;",
"1 -> 2;",
"2 -> 3;}"
)
)
);

final Dependency dependency = dependency(null, 1, 2);

assertThatThrownBy(() -> service.create(dependency))
.isInstanceOf(DuplicateDependencyException.class)
.hasMessageStartingWith("Dependency already exists")
.extracting("dependency", InstanceOfAssertFactories.type(Dependency.class))
.extracting(Dependency::getSource, Dependency::getTarget)
.containsExactlyInAnyOrder(
new MicroserviceBuilder().withId(1L).build(),
new MicroserviceBuilder().withId(2L).build()
);
}

@Test
void create_Success() {
given(graphLoaderService.loadGraph()).willReturn(
Expand Down Expand Up @@ -171,6 +199,36 @@ void update_WillIntroduceCircularDependencies_ExceptionIsThrown() {
.containsExactlyInAnyOrder(1L, 2L, 3L);
}

@Test
void update_DuplicateWillBeIntroduced_ExceptionIsThrown() {
given(graphLoaderService.loadGraph()).willReturn(
GraphUtils.createGraph(
String.join("\n",
"strict digraph G { ",
"1; 2; 3;",
"1 -> 2;",
"2 -> 3;}" // has id = 2L
)
)
);

given(repository.findById(2L))
.willReturn(Optional.of(dependency(2, 2, 3)));

final Dependency dependency = dependency(2, 1, 2);


assertThatThrownBy(() -> service.update(dependency))
.isInstanceOf(DuplicateDependencyException.class)
.hasMessageStartingWith("Dependency already exists")
.extracting("dependency", InstanceOfAssertFactories.type(Dependency.class))
.extracting(Dependency::getSource, Dependency::getTarget)
.containsExactlyInAnyOrder(
new MicroserviceBuilder().withId(1L).build(),
new MicroserviceBuilder().withId(2L).build()
);
}

@Test
void update_Success() {
given(graphLoaderService.loadGraph()).willReturn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ class DependencyBuilderTest {
@Test
void build() {
DependencyBuilder builder = new DependencyBuilder();
Dependency dependency = builder.withId(1L).withSource(2L).withTarget(3L).build();
assertThat(dependency).isNotNull().extracting(Dependency::getId).isEqualTo(1L);
Dependency dependency = builder
.withId(1L)
.withName("test")
.withSource(2L)
.withTarget(3L)
.build();

assertThat(dependency).isNotNull().extracting(Dependency::getId, Dependency::getName).containsExactly(1L, "test");
assertThat(dependency.getSource()).isNotNull().extracting(Microservice::getId).isEqualTo(2L);
assertThat(dependency.getTarget()).isNotNull().extracting(Microservice::getId).isEqualTo(3L);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class MicroserviceBuilderTest {
@Test
void build() {
MicroserviceBuilder builder = new MicroserviceBuilder();
Microservice microservice = builder.withId(1L).build();
assertThat(microservice).isNotNull().extracting(Microservice::getId).isEqualTo(1L);
Microservice microservice = builder.withId(1L).withName("Test microservice").build();
assertThat(microservice).isNotNull()
.extracting(Microservice::getId, Microservice::getName).containsExactly(1L, "Test microservice");
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.github.microcatalog.web.rest.errors.custom;

import com.github.microcatalog.service.custom.exceptions.CircularDependenciesException;
import com.github.microcatalog.service.custom.exceptions.DuplicateDependencyException;
import com.github.microcatalog.service.custom.exceptions.SelfCircularException;
import com.github.microcatalog.utils.DependencyBuilder;
import com.github.microcatalog.utils.MicroserviceBuilder;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
Expand All @@ -17,8 +19,7 @@
import java.util.Arrays;
import java.util.HashSet;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.spy;

@ExtendWith(MockitoExtension.class)
Expand All @@ -29,11 +30,37 @@ class ReleasePathAdviceTraitTest {
@Mock
private NativeWebRequest webRequest;

@Test
void handleDuplicateDependencyException() {
final DuplicateDependencyException exception = new DuplicateDependencyException("Test message",
new DependencyBuilder()
.withId(1L)
.withName("Test Dependency")
.withSource(1L)
.withTarget(2L)
.build());

ResponseEntity<Problem> response = cut.handleDuplicateDependencyException(exception, webRequest);

assertThat(response).isNotNull()
.extracting(HttpEntity::getBody).isNotNull();

assertThat(response.getStatusCodeValue()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.value());
assertThat(response.getBody()).isNotNull()
.extracting(Problem::getParameters, InstanceOfAssertFactories.map(String.class, Object.class))
.contains(
entry(ReleasePathAdviceTrait.EXCEPTION_KEY, exception.getClass().getSimpleName()),
entry(ReleasePathAdviceTrait.MESSAGE_KEY, "Test message"),
entry("dependencyId", "1"),
entry("dependencyName", "Test Dependency")
);
}

@Test
void handleSelfCircularException() {

final SelfCircularException exception =
new SelfCircularException("Test message", new MicroserviceBuilder().withId(1L).build());
new SelfCircularException("Test message", new MicroserviceBuilder().withId(1L).withName("Test").build());
ResponseEntity<Problem> response = cut.handleSelfCircularException(exception, webRequest);

assertThat(response).isNotNull()
Expand All @@ -44,19 +71,19 @@ void handleSelfCircularException() {
assertThat(response.getBody()).isNotNull()
.extracting(Problem::getParameters, InstanceOfAssertFactories.map(String.class, Object.class))
.contains(
entry(ReleasePathAdviceTrait.HEADER_KEY, exception.getClass().getName()),
entry(ReleasePathAdviceTrait.EXCEPTION_KEY, exception.getClass().getSimpleName()),
entry(ReleasePathAdviceTrait.MESSAGE_KEY, "Test message"),
entry(ReleasePathAdviceTrait.PAYLOAD_KEY, "id = 1, name = null")
entry("microserviceName", "Test")
);
}

@Test
void handleCircularDependenciesException() {
final CircularDependenciesException exception =
new CircularDependenciesException("Test message", new HashSet<>(Arrays.asList(
new MicroserviceBuilder().withId(1L).build(),
new MicroserviceBuilder().withId(2L).build(),
new MicroserviceBuilder().withId(3L).build()
new MicroserviceBuilder().withId(1L).withName("First").build(),
new MicroserviceBuilder().withId(2L).withName("Second").build(),
new MicroserviceBuilder().withId(3L).withName("Third").build()
)));

ResponseEntity<Problem> response = cut.handleCircularDependenciesException(exception, webRequest);
Expand All @@ -69,9 +96,9 @@ void handleCircularDependenciesException() {
assertThat(response.getBody()).isNotNull()
.extracting(Problem::getParameters, InstanceOfAssertFactories.map(String.class, Object.class))
.contains(
entry(ReleasePathAdviceTrait.HEADER_KEY, exception.getClass().getName()),
entry(ReleasePathAdviceTrait.EXCEPTION_KEY, exception.getClass().getSimpleName()),
entry(ReleasePathAdviceTrait.MESSAGE_KEY, "Test message"),
entry(ReleasePathAdviceTrait.PAYLOAD_KEY, "1,2,3")
entry("microservices", "First,Second,Third")
);
}
}

0 comments on commit acaf233

Please sign in to comment.