diff --git a/src/main/java/com/github/microcatalog/config/CacheConfiguration.java b/src/main/java/com/github/microcatalog/config/CacheConfiguration.java index 026465e..95ba768 100644 --- a/src/main/java/com/github/microcatalog/config/CacheConfiguration.java +++ b/src/main/java/com/github/microcatalog/config/CacheConfiguration.java @@ -5,6 +5,9 @@ import com.github.microcatalog.domain.custom.ReleaseGroup; import com.github.microcatalog.domain.custom.ReleasePath; import com.github.microcatalog.domain.custom.ReleaseStep; +import com.github.microcatalog.domain.custom.impact.analysis.Group; +import com.github.microcatalog.domain.custom.impact.analysis.Item; +import com.github.microcatalog.domain.custom.impact.analysis.Result; import org.ehcache.config.builders.*; import org.ehcache.jsr107.Eh107Configuration; @@ -60,6 +63,11 @@ public JCacheManagerCustomizer cacheManagerCustomizer() { createCache(cm, ReleaseGroup.class.getName() + ".steps"); createCache(cm, ReleasePath.class.getName()); createCache(cm, ReleasePath.class.getName() + ".groups"); + createCache(cm, Item.class.getName()); + createCache(cm, Group.class.getName()); + createCache(cm, Group.class.getName() + ".items"); + createCache(cm, Result.class.getName()); + createCache(cm, Result.class.getName() + ".groups"); // jhipster-needle-ehcache-add-entry }; } diff --git a/src/main/java/com/github/microcatalog/domain/custom/ReleaseGroup.java b/src/main/java/com/github/microcatalog/domain/custom/ReleaseGroup.java index ad497ef..99fb725 100644 --- a/src/main/java/com/github/microcatalog/domain/custom/ReleaseGroup.java +++ b/src/main/java/com/github/microcatalog/domain/custom/ReleaseGroup.java @@ -4,6 +4,8 @@ import org.hibernate.annotations.CacheConcurrencyStrategy; import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; import java.util.Set; /** @@ -35,4 +37,10 @@ public ReleaseGroup removeSteps(ReleaseStep releaseStep) { public void setSteps(Set releaseSteps) { this.steps = releaseSteps; } + + public Optional findByTargetId(final long targetId) { + return steps.stream() + .filter(i -> Objects.equals(targetId, i.getWorkItem().getId())) + .findAny(); + } } diff --git a/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Group.java b/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Group.java new file mode 100644 index 0000000..c8548de --- /dev/null +++ b/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Group.java @@ -0,0 +1,41 @@ +package com.github.microcatalog.domain.custom.impact.analysis; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class Group { + private Set items = new HashSet<>(); + + public Set getItems() { + return items; + } + + public Group items(Set items) { + this.items = items; + return this; + } + + public void addItem(final Item item) { + if (item == null) { + return; + } + + this.items.add(item); + } + + public void setItems(Set items) { + this.items = items; + } + + public Optional findByTargetId(final long targetId) { + return items.stream() + .filter(i -> Objects.equals(targetId, i.getTarget().getId())) + .findAny(); + } +} diff --git a/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Item.java b/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Item.java new file mode 100644 index 0000000..efd435b --- /dev/null +++ b/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Item.java @@ -0,0 +1,39 @@ +package com.github.microcatalog.domain.custom.impact.analysis; + +import com.github.microcatalog.domain.Microservice; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import java.util.List; + +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class Item { + private Microservice target; + private List siblings; + + public Microservice getTarget() { + return target; + } + + public Item target(Microservice microservice) { + this.target = microservice; + return this; + } + + public void setTarget(Microservice microservice) { + this.target = microservice; + } + + public List getSiblings() { + return siblings; + } + + public Item siblings(List siblings) { + this.siblings = siblings; + return this; + } + + public void setSiblings(List siblings) { + this.siblings = siblings; + } +} diff --git a/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Result.java b/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Result.java new file mode 100644 index 0000000..c3486fc --- /dev/null +++ b/src/main/java/com/github/microcatalog/domain/custom/impact/analysis/Result.java @@ -0,0 +1,63 @@ +package com.github.microcatalog.domain.custom.impact.analysis; + +import com.github.microcatalog.domain.Microservice; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class Result { + private Instant createdOn; + private List groups = new ArrayList<>(); + private Microservice target; + + public Instant getCreatedOn() { + return createdOn; + } + + public Result createdOn(Instant createdOn) { + this.createdOn = createdOn; + return this; + } + + public void setCreatedOn(Instant createdOn) { + this.createdOn = createdOn; + } + + public List getGroups() { + return groups; + } + + public Result groups(List releaseGroups) { + this.groups = releaseGroups; + return this; + } + + public void addGroup(final Group group) { + if (group == null) { + return; + } + + this.groups.add(group); + } + + public void setGroups(List releaseGroups) { + this.groups = releaseGroups; + } + + public Microservice getTarget() { + return target; + } + + public Result target(Microservice microservice) { + this.target = microservice; + return this; + } + + public void setTarget(Microservice microservice) { + this.target = microservice; + } +} diff --git a/src/main/java/com/github/microcatalog/service/custom/GraphOperationsService.java b/src/main/java/com/github/microcatalog/service/custom/GraphOperationsService.java new file mode 100644 index 0000000..4f30fb9 --- /dev/null +++ b/src/main/java/com/github/microcatalog/service/custom/GraphOperationsService.java @@ -0,0 +1,85 @@ +package com.github.microcatalog.service.custom; + +import com.github.microcatalog.domain.Microservice; +import com.github.microcatalog.service.custom.exceptions.MicroserviceNotFoundException; +import org.jgrapht.Graph; +import org.jgrapht.alg.connectivity.ConnectivityInspector; +import org.jgrapht.alg.cycle.CycleDetector; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.DefaultEdge; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public abstract class GraphOperationsService { + + private final Logger log = LoggerFactory.getLogger(GraphOperationsService.class); + + protected final GraphLoaderService graphLoaderService; + + protected GraphOperationsService(GraphLoaderService graphLoaderService) { + this.graphLoaderService = graphLoaderService; + } + + protected GraphContext getConnectedSubgraphWithoutCycles(final long microserviceId) { + final Graph graph = graphLoaderService.loadGraph(); + + if (graph.vertexSet().isEmpty()) { + return new GraphContext(graph, null); + } + + final Optional maybeTarget = graph.vertexSet() + .stream().filter(v -> Objects.equals(v.getId(), microserviceId)).findFirst(); + + // can't build release path, cause microservice with given id is not present in graph + if (!maybeTarget.isPresent()) { + throw new MicroserviceNotFoundException("Microservice not found", microserviceId); + } + + final Microservice target = maybeTarget.get(); + + final ConnectivityInspector inspector = new ConnectivityInspector<>(graph); + final Set connectedSet = inspector.connectedSetOf(target); + + // Connected subgraph, that contains target microservice + final AsSubgraph targetSubgraph = new AsSubgraph<>(graph, connectedSet); + log.debug("Connected subgraph, that contains target microservice: {}", targetSubgraph); + + final CycleDetector cycleDetector = new CycleDetector<>(targetSubgraph); + if (cycleDetector.detectCycles()) { + final Set cycles = cycleDetector.findCycles(); + throw new IllegalArgumentException(String.format("There are cyclic dependencies between microservices : %s", cycles)); + } + + return new GraphContext(targetSubgraph, target); + } + + protected static class GraphContext { + private final Graph graph; + private final Microservice target; + + public GraphContext(Graph graph, Microservice target) { + this.graph = graph; + this.target = target; + } + + public boolean hasEmptyGraph() { + if (this.graph == null) { + return true; + } + + return graph.vertexSet().isEmpty(); + } + + public Graph getGraph() { + return graph; + } + + public Microservice getTarget() { + return target; + } + } +} diff --git a/src/main/java/com/github/microcatalog/service/custom/ImpactAnalysisService.java b/src/main/java/com/github/microcatalog/service/custom/ImpactAnalysisService.java new file mode 100644 index 0000000..6ac23d3 --- /dev/null +++ b/src/main/java/com/github/microcatalog/service/custom/ImpactAnalysisService.java @@ -0,0 +1,83 @@ +package com.github.microcatalog.service.custom; + +import com.github.microcatalog.domain.Microservice; +import com.github.microcatalog.domain.custom.impact.analysis.Group; +import com.github.microcatalog.domain.custom.impact.analysis.Item; +import com.github.microcatalog.domain.custom.impact.analysis.Result; +import org.jgrapht.Graph; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.EdgeReversedGraph; +import org.jgrapht.traverse.DepthFirstIterator; +import org.jgrapht.traverse.GraphIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Transactional +public class ImpactAnalysisService extends GraphOperationsService { + + private final Logger log = LoggerFactory.getLogger(ImpactAnalysisService.class); + + public ImpactAnalysisService(GraphLoaderService graphLoaderService) { + super(graphLoaderService); + } + + public Optional calculate(final Long microserviceId) { + final GraphContext context = getConnectedSubgraphWithoutCycles(microserviceId); + if (context.hasEmptyGraph()) { + return Optional.empty(); + } + + final Graph reversed = new EdgeReversedGraph<>(context.getGraph()); + + // Calculate all vertices, you can reach from target + final Set affectedMicroservices = new HashSet<>(); + GraphIterator iterator = new DepthFirstIterator<>(reversed, context.getTarget()); + while (iterator.hasNext()) { + affectedMicroservices.add(iterator.next()); + } + + final Graph affectedGraph = new AsSubgraph<>(reversed, affectedMicroservices); + + final Result result = new Result().createdOn(Instant.now()).target(context.getTarget()); + + do { + final List verticesWithoutIncomingEdges = affectedGraph.vertexSet().stream() + .filter(v -> affectedGraph.incomingEdgesOf(v).isEmpty()) + .collect(Collectors.toList()); + log.debug("Leaves: {}", verticesWithoutIncomingEdges); + + final Group group = createGroup(affectedGraph, verticesWithoutIncomingEdges); + result.addGroup(group); + + verticesWithoutIncomingEdges.forEach(affectedGraph::removeVertex); + } while (!affectedGraph.vertexSet().isEmpty()); + + return Optional.of(result); + } + + private Group createGroup(final Graph graph, final List verticesWithoutIncomingEdges) { + final Group group = new Group(); + + verticesWithoutIncomingEdges.forEach(v -> { + final Set outgoingEdgesOf = graph.outgoingEdgesOf(v); + final List siblings = new ArrayList<>(); + outgoingEdgesOf.forEach(e -> { + final Microservice sibling = graph.getEdgeTarget(e); + siblings.add(sibling); + }); + + group.addItem(new Item().target(v).siblings(siblings)); + }); + + return group; + } + +} diff --git a/src/main/java/com/github/microcatalog/service/custom/ReleasePathCustomService.java b/src/main/java/com/github/microcatalog/service/custom/ReleasePathCustomService.java index 501c15f..855807a 100644 --- a/src/main/java/com/github/microcatalog/service/custom/ReleasePathCustomService.java +++ b/src/main/java/com/github/microcatalog/service/custom/ReleasePathCustomService.java @@ -5,8 +5,6 @@ import com.github.microcatalog.domain.custom.ReleasePath; import com.github.microcatalog.domain.custom.ReleaseStep; import org.jgrapht.Graph; -import org.jgrapht.alg.connectivity.ConnectivityInspector; -import org.jgrapht.alg.cycle.CycleDetector; import org.jgrapht.graph.AsSubgraph; import org.jgrapht.graph.DefaultEdge; import org.jgrapht.graph.EdgeReversedGraph; @@ -16,7 +14,6 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import javax.transaction.Transactional; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -25,58 +22,31 @@ * Service for release path calculation */ @Service -@Transactional -public class ReleasePathCustomService { +public class ReleasePathCustomService extends GraphOperationsService { private final Logger log = LoggerFactory.getLogger(ReleasePathCustomService.class); - private final GraphLoaderService graphLoaderService; - public ReleasePathCustomService(GraphLoaderService graphLoaderService) { - this.graphLoaderService = graphLoaderService; + super(graphLoaderService); } - public Optional getReleasePath(final Long microserviceId) { - final Graph graph = graphLoaderService.loadGraph(); - - final Optional maybeTarget = graph.vertexSet() - .stream().filter(v -> Objects.equals(v.getId(), microserviceId)).findFirst(); - - // can't build release path, cause microservice with given id is not present in graph - if (!maybeTarget.isPresent()) { + final GraphContext context = getConnectedSubgraphWithoutCycles(microserviceId); + if (context.hasEmptyGraph()) { return Optional.empty(); } - final Microservice target = maybeTarget.get(); - - final ConnectivityInspector inspector = new ConnectivityInspector<>(graph); - final Set connectedSet = inspector.connectedSetOf(target); - - // Connected subgraph, that contains target microservice - final AsSubgraph targetSubgraph = new AsSubgraph<>(graph, connectedSet); - log.debug("Connected subgraph, that contains target microservice: {}", targetSubgraph); - - final CycleDetector cycleDetector = new CycleDetector<>(targetSubgraph); - if (cycleDetector.detectCycles()) { - final Set cycles = cycleDetector.findCycles(); - throw new IllegalArgumentException(String.format("There are cyclic dependencies between microservices : %s", cycles)); - } - final Set pathMicroservices = new HashSet<>(); - GraphIterator iterator = new DepthFirstIterator<>(targetSubgraph, target); + GraphIterator iterator = new DepthFirstIterator<>(context.getGraph(), context.getTarget()); while (iterator.hasNext()) { pathMicroservices.add(iterator.next()); } - // TODO new use-case and visualisation - // For each element of pathSet calculate all nodes who depends on items from pathSet. Microservices which will be possibly affected if we build target and it's direct dependencies - - final Graph pathGraph = new AsSubgraph<>(targetSubgraph, pathMicroservices); + final Graph pathGraph = new AsSubgraph<>(context.getGraph(), pathMicroservices); log.debug("Connected subgraph, which contains all paths from target microservice to it's dependencies {}", pathGraph); final Graph reversed = new EdgeReversedGraph<>(pathGraph); - return Optional.of(convert(reversed, target)); + return Optional.of(convert(reversed, context.getTarget())); } private ReleasePath convert(final Graph graph, final Microservice target) { diff --git a/src/main/java/com/github/microcatalog/service/custom/exceptions/MicroserviceNotFoundException.java b/src/main/java/com/github/microcatalog/service/custom/exceptions/MicroserviceNotFoundException.java new file mode 100644 index 0000000..9da119f --- /dev/null +++ b/src/main/java/com/github/microcatalog/service/custom/exceptions/MicroserviceNotFoundException.java @@ -0,0 +1,15 @@ +package com.github.microcatalog.service.custom.exceptions; + +public class MicroserviceNotFoundException extends RuntimeException { + private final long microserviceId; + + public MicroserviceNotFoundException(String message, long microserviceId) { + super(message); + + this.microserviceId = microserviceId; + } + + public long getMicroserviceId() { + return microserviceId; + } +} diff --git a/src/main/java/com/github/microcatalog/web/rest/custom/ImpactAnalysisCustomResource.java b/src/main/java/com/github/microcatalog/web/rest/custom/ImpactAnalysisCustomResource.java new file mode 100644 index 0000000..0541103 --- /dev/null +++ b/src/main/java/com/github/microcatalog/web/rest/custom/ImpactAnalysisCustomResource.java @@ -0,0 +1,43 @@ +package com.github.microcatalog.web.rest.custom; + +import com.github.microcatalog.domain.custom.impact.analysis.Result; +import com.github.microcatalog.service.custom.ImpactAnalysisService; +import io.github.jhipster.web.util.ResponseUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; + +/** + * REST controller for calculation of impact analysis {@link Result} + */ +@RestController +@RequestMapping("/api") +public class ImpactAnalysisCustomResource { + + private final Logger log = LoggerFactory.getLogger(ImpactAnalysisCustomResource.class); + + private final ImpactAnalysisService service; + + public ImpactAnalysisCustomResource(ImpactAnalysisService service) { + this.service = service; + } + + /** + * {@code GET /impact-analysis/microservice/:microserviceId} : get the impact analysis for "microserviceId" + * + * @param microserviceId id of Microservice for which impact analysis should be performed + * @return impact analysis calculated for given microservice + */ + @GetMapping("/impact-analysis/microservice/{microserviceId}") + public ResponseEntity calculate(@PathVariable Long microserviceId) { + log.debug("REST request to get impact analysis Result for microserviceId : {}", microserviceId); + final Optional impactAnalysisResult = service.calculate(microserviceId); + return ResponseUtil.wrapOrNotFound(impactAnalysisResult); + } +} diff --git a/src/main/webapp/app/dashboard/dashboard-routing.module.ts b/src/main/webapp/app/dashboard/dashboard-routing.module.ts index 8dd37ea..7a01e63 100644 --- a/src/main/webapp/app/dashboard/dashboard-routing.module.ts +++ b/src/main/webapp/app/dashboard/dashboard-routing.module.ts @@ -12,6 +12,10 @@ import { RouterModule } from '@angular/router'; path: 'release-path', loadChildren: () => import('./release-path-dashboard/release-path-dashboard.module').then(m => m.ReleasePathDashboardModule), }, + { + path: 'impact-analysis', + loadChildren: () => import('./impact-analysis/impact-analysis-dashboard.module').then(m => m.ImpactAnalysisDashboardModule), + }, ]), ], }) diff --git a/src/main/webapp/app/dashboard/dependency-dashboard/create-dependency-dialog/create-dependency-dialog.component.html b/src/main/webapp/app/dashboard/dependency-dashboard/create-dependency-dialog/create-dependency-dialog.component.html index f9bc164..8ccd946 100644 --- a/src/main/webapp/app/dashboard/dependency-dashboard/create-dependency-dialog/create-dependency-dialog.component.html +++ b/src/main/webapp/app/dashboard/dependency-dashboard/create-dependency-dialog/create-dependency-dialog.component.html @@ -10,14 +10,14 @@
-
-
diff --git a/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.html b/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.html index 9ff33d0..0ffe92f 100644 --- a/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.html +++ b/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.html @@ -55,6 +55,9 @@ + diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.component.html b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.component.html new file mode 100644 index 0000000..25de6d6 --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.component.html @@ -0,0 +1,11 @@ +
+
+

Impact analysis

+ +
+
+ +
+
+ + diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.component.scss b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.component.ts b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.component.ts new file mode 100644 index 0000000..a3659f3 --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'jhi-impact-analysis-dashboard', + templateUrl: './impact-analysis-dashboard.component.html', + styleUrls: ['./impact-analysis-dashboard.component.scss'], +}) +export class ImpactAnalysisDashboardComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} +} diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.module.ts b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.module.ts new file mode 100644 index 0000000..7cde1e8 --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { MicrocatalogSharedModule } from 'app/shared/shared.module'; +import { ImpactAnalysisDashboardComponent } from './impact-analysis-dashboard.component'; +import { RouterModule } from '@angular/router'; +import { impactAnalysisDashboardRoute } from './impact-analysis-dashboard.route'; +import { ImpactAnalysisGraphModule } from './impact-analysis-graph/impact-analysis-graph.module'; +import { ImpactAnalysisLegendModule } from './impact-analysis-legend/impact-analysis-legend.module'; + +@NgModule({ + imports: [ + MicrocatalogSharedModule, + ImpactAnalysisGraphModule, + ImpactAnalysisLegendModule, + RouterModule.forChild(impactAnalysisDashboardRoute), + ], + declarations: [ImpactAnalysisDashboardComponent], +}) +export class ImpactAnalysisDashboardModule {} diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.route.ts b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.route.ts new file mode 100644 index 0000000..1c53e5f --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-dashboard.route.ts @@ -0,0 +1,51 @@ +import { ActivatedRouteSnapshot, Resolve, Router, Routes } from '@angular/router'; +import { ImpactAnalysisDashboardComponent } from './impact-analysis-dashboard.component'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; +import { Injectable } from '@angular/core'; +import { EMPTY, Observable, of } from 'rxjs'; +import { catchError, flatMap } from 'rxjs/operators'; +import { HttpResponse } from '@angular/common/http'; +import { IResult, Result } from 'app/shared/model/impact/analysis/result.model'; +import { ImpactAnalysisCustomService } from 'app/entities/release-path/custom/impact-analysis-custom.service'; + +@Injectable({ providedIn: 'root' }) +export class ImpactAnalysisResultResolve implements Resolve { + constructor(private service: ImpactAnalysisCustomService, private router: Router) {} + + resolve(route: ActivatedRouteSnapshot): Observable | Observable { + const id = route.params['id']; + if (id) { + return this.service.find(id).pipe( + flatMap((analysisResult: HttpResponse) => { + if (analysisResult.body) { + return of(analysisResult.body); + } else { + this.router.navigate(['404']); + return EMPTY; + } + }), + catchError(error => { + alert('Error building release path. ' + error.error.detail); + return EMPTY; + }) + ); + } + return of(new Result()); + } +} + +export const impactAnalysisDashboardRoute: Routes = [ + { + path: ':id', + component: ImpactAnalysisDashboardComponent, + resolve: { + analysisResult: ImpactAnalysisResultResolve, + }, + data: { + authorities: [Authority.USER], + pageTitle: 'microcatalogApp.microservice.home.title', + }, + canActivate: [UserRouteAccessService], + }, +]; diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.component.html b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.component.html new file mode 100644 index 0000000..2971e26 --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.component.html @@ -0,0 +1 @@ +
diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.component.scss b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.component.ts b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.component.ts new file mode 100644 index 0000000..f92629a --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.component.ts @@ -0,0 +1,72 @@ +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IResult } from '../../../shared/model/impact/analysis/result.model'; +import { VisNetworkService } from '../../../shared/vis/vis-network.service'; +import { DataSet } from 'vis-data/peer'; +import { NodeColorsService } from '../../release-path-dashboard/node-colors.service'; + +@Component({ + selector: 'jhi-impact-analysis-graph', + templateUrl: './impact-analysis-graph.component.html', + styleUrls: ['./impact-analysis-graph.component.scss'], +}) +export class ImpactAnalysisGraphComponent implements OnInit, AfterViewInit { + @ViewChild('visNetwork', { static: false }) + visNetwork!: ElementRef; + + networkInstance: any; + + analysisResult?: IResult; + + constructor( + protected activatedRoute: ActivatedRoute, + protected visNetworkService: VisNetworkService, + protected nodeColorsService: NodeColorsService + ) {} + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ analysisResult }) => { + this.analysisResult = analysisResult; + }); + } + + ngAfterViewInit(): void { + const container = this.visNetwork; + + this.networkInstance = this.visNetworkService.createNetwork(container); + + const nodes = new DataSet(); + const edges = new DataSet(); + const analysisTargetId = this.analysisResult?.target?.id; + + if (this.analysisResult) { + let groupIndex = 0; + this.analysisResult.groups?.forEach(g => { + g.items?.forEach(i => { + const itemTargetId = i.target?.id; + let nodeColor = this.nodeColorsService.getColor(groupIndex); + if (analysisTargetId === itemTargetId) { + nodeColor = this.nodeColorsService.getActiveColor(); + } else { + ++groupIndex; + } + + nodes.add({ + id: itemTargetId, + label: i.target?.name, + color: nodeColor, + }); + + i.siblings?.forEach(s => { + edges.add({ + from: itemTargetId, + to: s.id, + }); + }); + }); + }); + } + + this.networkInstance.setData({ nodes, edges }); + } +} diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.module.ts b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.module.ts new file mode 100644 index 0000000..57c1ecb --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-graph/impact-analysis-graph.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { ImpactAnalysisGraphComponent } from './impact-analysis-graph.component'; +import { MicrocatalogSharedModule } from '../../../shared/shared.module'; + +@NgModule({ + declarations: [ImpactAnalysisGraphComponent], + imports: [MicrocatalogSharedModule], + exports: [ImpactAnalysisGraphComponent], +}) +export class ImpactAnalysisGraphModule {} diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.component.html b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.component.html new file mode 100644 index 0000000..2f3c805 --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.component.html @@ -0,0 +1,23 @@ +
+

Analysis target:

+

{{analysisResult?.target?.name}}

+
+
+ +
+
+

Dependencies of target (total {{dependencies.length}}):

+
+
+
    +
  • + {{d.name}} +
  • +
+
+
+ +
diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.component.scss b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.component.ts b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.component.ts new file mode 100644 index 0000000..2890f19 --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { IResult } from '../../../shared/model/impact/analysis/result.model'; +import { ActivatedRoute } from '@angular/router'; +import { IMicroservice } from '../../../shared/model/microservice.model'; + +@Component({ + selector: 'jhi-impact-analysis-legend', + templateUrl: './impact-analysis-legend.component.html', + styleUrls: ['./impact-analysis-legend.component.scss'], +}) +export class ImpactAnalysisLegendComponent implements OnInit { + analysisResult?: IResult; + dependencies: IMicroservice[] = []; + page = 1; + pageSize = 15; + + constructor(protected activatedRoute: ActivatedRoute) {} + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ analysisResult }) => { + this.analysisResult = analysisResult; + + this.initDependencies(); + }); + } + + initDependencies(): void { + this.dependencies = []; + this.analysisResult?.groups?.forEach(v => v.items?.forEach(i => this.dependencies.push(i.target))); + } +} diff --git a/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.module.ts b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.module.ts new file mode 100644 index 0000000..5bef50e --- /dev/null +++ b/src/main/webapp/app/dashboard/impact-analysis/impact-analysis-legend/impact-analysis-legend.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { MicrocatalogSharedModule } from 'app/shared/shared.module'; +import { ImpactAnalysisLegendComponent } from './impact-analysis-legend.component'; + +@NgModule({ + declarations: [ImpactAnalysisLegendComponent], + imports: [MicrocatalogSharedModule], + exports: [ImpactAnalysisLegendComponent], +}) +export class ImpactAnalysisLegendModule {} diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.html b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.html index 37cc5b8..35b5dfb 100644 --- a/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.html +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.html @@ -1,5 +1,6 @@
+

Release path

diff --git a/src/main/webapp/app/entities/microservice/microservice-dashboard/microservice-search/microservice-search.component.ts b/src/main/webapp/app/entities/microservice/microservice-dashboard/microservice-search/microservice-search.component.ts index 7b39f55..e8ea5a2 100644 --- a/src/main/webapp/app/entities/microservice/microservice-dashboard/microservice-search/microservice-search.component.ts +++ b/src/main/webapp/app/entities/microservice/microservice-dashboard/microservice-search/microservice-search.component.ts @@ -25,7 +25,7 @@ export class MicroserviceSearchComponent implements OnInit { ngOnInit(): void {} @Input() - set initialValue(value: IMicroservice) { + set selectedValue(value: IMicroservice) { this.model = value; } diff --git a/src/main/webapp/app/entities/release-path/custom/impact-analysis-custom.service.ts b/src/main/webapp/app/entities/release-path/custom/impact-analysis-custom.service.ts new file mode 100644 index 0000000..23ef9cd --- /dev/null +++ b/src/main/webapp/app/entities/release-path/custom/impact-analysis-custom.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { SERVER_API_URL } from '../../../app.constants'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import * as moment from 'moment'; +import { IResult } from '../../../shared/model/impact/analysis/result.model'; + +type EntityResponseType = HttpResponse; + +@Injectable({ + providedIn: 'root', +}) +export class ImpactAnalysisCustomService { + public resourceUrl = SERVER_API_URL + 'api/impact-analysis'; + + constructor(protected http: HttpClient) {} + + find(microserviceId: number): Observable { + return this.http + .get(`${this.resourceUrl}/microservice/${microserviceId}`, { observe: 'response' }) + .pipe(map((res: EntityResponseType) => this.convertDateFromServer(res))); + } + + protected convertDateFromServer(res: EntityResponseType): EntityResponseType { + if (res.body) { + res.body.createdOn = res.body.createdOn ? moment(res.body.createdOn) : undefined; + } + return res; + } +} diff --git a/src/main/webapp/app/shared/model/impact/analysis/group.model.ts b/src/main/webapp/app/shared/model/impact/analysis/group.model.ts new file mode 100644 index 0000000..966b162 --- /dev/null +++ b/src/main/webapp/app/shared/model/impact/analysis/group.model.ts @@ -0,0 +1,5 @@ +import { IItem } from './item.model'; + +export interface IGroup { + items: IItem[]; +} diff --git a/src/main/webapp/app/shared/model/impact/analysis/item.model.ts b/src/main/webapp/app/shared/model/impact/analysis/item.model.ts new file mode 100644 index 0000000..79b36ae --- /dev/null +++ b/src/main/webapp/app/shared/model/impact/analysis/item.model.ts @@ -0,0 +1,6 @@ +import { IMicroservice } from '../../microservice.model'; + +export interface IItem { + target: IMicroservice; + siblings: IMicroservice[]; +} diff --git a/src/main/webapp/app/shared/model/impact/analysis/result.model.ts b/src/main/webapp/app/shared/model/impact/analysis/result.model.ts new file mode 100644 index 0000000..7a68c00 --- /dev/null +++ b/src/main/webapp/app/shared/model/impact/analysis/result.model.ts @@ -0,0 +1,13 @@ +import { Moment } from 'moment'; +import { IMicroservice } from '../../microservice.model'; +import { IGroup } from './group.model'; + +export interface IResult { + createdOn?: Moment; + target?: IMicroservice; + groups?: IGroup[]; +} + +export class Result implements IResult { + constructor(public createdOn?: Moment, public groups?: IGroup[], public target?: IMicroservice) {} +} diff --git a/src/test/java/com/github/microcatalog/MockMvcWithUser.java b/src/test/java/com/github/microcatalog/MockMvcWithUser.java new file mode 100644 index 0000000..8566c3d --- /dev/null +++ b/src/test/java/com/github/microcatalog/MockMvcWithUser.java @@ -0,0 +1,18 @@ +package com.github.microcatalog; + +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.security.test.context.support.WithMockUser; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@AutoConfigureWebMvc +@AutoConfigureMockMvc +@WithMockUser +public @interface MockMvcWithUser { +} diff --git a/src/test/java/com/github/microcatalog/service/custom/GraphOperationsServiceTest.java b/src/test/java/com/github/microcatalog/service/custom/GraphOperationsServiceTest.java new file mode 100644 index 0000000..8ac2e23 --- /dev/null +++ b/src/test/java/com/github/microcatalog/service/custom/GraphOperationsServiceTest.java @@ -0,0 +1,14 @@ +package com.github.microcatalog.service.custom; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class GraphOperationsServiceTest { + + @Test + void graphContext_NullGraph() { + final GraphOperationsService.GraphContext context = new GraphOperationsService.GraphContext(null, null); + assertThat(context.hasEmptyGraph()).isTrue(); + } +} diff --git a/src/test/java/com/github/microcatalog/service/custom/ImpactAnalysisServiceTest.java b/src/test/java/com/github/microcatalog/service/custom/ImpactAnalysisServiceTest.java new file mode 100644 index 0000000..3eccf4a --- /dev/null +++ b/src/test/java/com/github/microcatalog/service/custom/ImpactAnalysisServiceTest.java @@ -0,0 +1,204 @@ +package com.github.microcatalog.service.custom; + +import com.github.microcatalog.domain.Microservice; +import com.github.microcatalog.domain.custom.impact.analysis.Group; +import com.github.microcatalog.domain.custom.impact.analysis.Item; +import com.github.microcatalog.domain.custom.impact.analysis.Result; +import com.github.microcatalog.service.GraphUtils; +import com.github.microcatalog.service.custom.exceptions.MicroserviceNotFoundException; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; +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.mock.mockito.MockBean; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.given; + +@SpringBootTest(classes = {ImpactAnalysisService.class}) +class ImpactAnalysisServiceTest { + + @MockBean + private GraphLoaderService graphLoaderService; + + @Autowired + private ImpactAnalysisService sut; + + @Test + void calculate_EmptyGraph_EmptyResult() { + given(graphLoaderService.loadGraph()) + .willReturn(new DefaultDirectedGraph<>(DefaultEdge.class)); + + Optional result = sut.calculate(4L); + assertThat(result).isNotNull().isNotPresent(); + } + + @Test + void calculate_NodeOutsideGraph_EmptyResult() { + given(graphLoaderService.loadGraph()) + .willReturn( + GraphUtils.createGraph(String.join("\n", + "strict digraph G { ", + "1; 2; 3;", + "1 -> 2;", + "2 -> 3;}" + ) + ) + ); + + assertThatThrownBy(() -> sut.calculate(4L)) + .isInstanceOf(MicroserviceNotFoundException.class) + .hasMessageStartingWith("Microservice not found") + .asInstanceOf(InstanceOfAssertFactories.type(MicroserviceNotFoundException.class)) + .extracting(MicroserviceNotFoundException::getMicroserviceId) + .isEqualTo(4L); + } + + @Test + void calculate_NoCycles_Success() { + given(graphLoaderService.loadGraph()) + .willReturn( + GraphUtils.createGraph(String.join("\n", + "strict digraph G { ", + "1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12;", + "1 -> 2;", + "2 -> 4;", + "6 -> 4;", + "4 -> 5;", + "4 -> 7;", + "4 -> 8;", + "3 -> 9;", + "3 -> 11;", + "12 -> 1;", + "7 -> 5;", + "10 -> 1;}" + ) + ) + ); + + final Optional maybeResult = sut.calculate(5L); + assertThat(maybeResult).isNotNull().isPresent(); + + final Result result = maybeResult.get(); + assertThat(result.getGroups()).isNotEmpty().hasSize(6); + + final List groups = result.getGroups(); + + Group group = groups.get(0); + assertThat(group.getItems()).isNotEmpty().hasSize(1); + Item item = group.findByTargetId(5L).orElseThrow(NoSuchElementException::new); + assertThatItemHasSiblings(item, 4L, 7L); + + group = groups.get(1); + assertThat(group.getItems()).isNotEmpty().hasSize(1); + item = group.findByTargetId(7L).orElseThrow(NoSuchElementException::new); + assertThatItemHasSiblings(item, 4L); + + group = groups.get(2); + assertThat(group.getItems()).isNotEmpty().hasSize(1); + item = group.findByTargetId(4L).orElseThrow(NoSuchElementException::new); + assertThatItemHasSiblings(item, 2L, 6L); + + group = groups.get(3); + assertThat(group.getItems()).isNotEmpty().hasSize(2); + item = group.findByTargetId(6L).orElseThrow(NoSuchElementException::new); + assertThatItemHasNoSiblings(item); + item = group.findByTargetId(2L).orElseThrow(NoSuchElementException::new); + assertThatItemHasSiblings(item, 1L); + + group = groups.get(4); + assertThat(group.getItems()).isNotEmpty().hasSize(1); + item = group.findByTargetId(1L).orElseThrow(NoSuchElementException::new); + assertThatItemHasSiblings(item, 10L, 12L); + + group = groups.get(5); + assertThat(group.getItems()).isNotEmpty().hasSize(2); + item = group.findByTargetId(10L).orElseThrow(NoSuchElementException::new); + assertThatItemHasNoSiblings(item); + item = group.findByTargetId(12L).orElseThrow(NoSuchElementException::new); + assertThatItemHasNoSiblings(item); + } + + @Test + void calculate_ContainsCyclesInSameComponent_ExceptionIsThrown() { + given(graphLoaderService.loadGraph()) + .willReturn( + GraphUtils.createGraph(String.join("\n", + "strict digraph G { ", + "1; 2; 3; 5; 6; 7;", + "1 -> 2;", + "2 -> 3;", + "3 -> 1;", + "5 -> 6;", + "5 -> 7;}" + ) + ) + ); + + assertThatIllegalArgumentException() + .isThrownBy(() -> + sut.calculate(1L) + ) + .withMessageStartingWith("There are cyclic dependencies between microservices"); + } + + + @Test + void calculate_ContainsCyclesInOtherComponent_Success() { + given(graphLoaderService.loadGraph()) + .willReturn( + GraphUtils.createGraph(String.join("\n", + "strict digraph G { ", + "1; 2; 3; 4; 5; 6; 7; 8;", + "1 -> 2;", + "2 -> 3;", + "2 -> 4;", + "5 -> 6;", + "6 -> 7;", + "7 -> 8;", + "8 -> 6;}" + ) + ) + ); + + Optional maybeResult = sut.calculate(4L); + assertThat(maybeResult).isPresent(); + + Result result = maybeResult.get(); + assertThat(result.getGroups()).isNotEmpty().hasSize(3); + + final List groups = result.getGroups(); + + Group group = groups.get(0); + assertThat(group.getItems()).isNotEmpty().hasSize(1); + Item item = group.findByTargetId(4L).orElseThrow(NoSuchElementException::new); + assertThatItemHasSiblings(item, 2L); + + group = groups.get(1); + assertThat(group.getItems()).isNotEmpty().hasSize(1); + item = group.findByTargetId(2L).orElseThrow(NoSuchElementException::new); + assertThatItemHasSiblings(item, 1L); + + group = groups.get(2); + assertThat(group.getItems()).isNotEmpty().hasSize(1); + item = group.findByTargetId(1L).orElseThrow(NoSuchElementException::new); + assertThatItemHasNoSiblings(item); + } + + private void assertThatItemHasNoSiblings(Item item) { + assertThat(item.getSiblings()).isNotNull().isEmpty(); + } + + private void assertThatItemHasSiblings(Item item, Long... siblingsIds) { + assertThat(item.getSiblings()) + .isNotNull() + .extracting(Microservice::getId) + .containsExactlyInAnyOrder(siblingsIds); + } +} diff --git a/src/test/java/com/github/microcatalog/service/custom/ReleasePathCustomServiceTest.java b/src/test/java/com/github/microcatalog/service/custom/ReleasePathCustomServiceTest.java index f9e09c4..c54411d 100644 --- a/src/test/java/com/github/microcatalog/service/custom/ReleasePathCustomServiceTest.java +++ b/src/test/java/com/github/microcatalog/service/custom/ReleasePathCustomServiceTest.java @@ -5,17 +5,20 @@ import com.github.microcatalog.domain.custom.ReleasePath; import com.github.microcatalog.domain.custom.ReleaseStep; import com.github.microcatalog.service.GraphUtils; +import com.github.microcatalog.service.custom.exceptions.MicroserviceNotFoundException; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; 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.mock.mockito.MockBean; -import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; import java.util.Optional; -import java.util.Set; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.given; @SpringBootTest(classes = {ReleasePathCustomService.class}) @@ -25,7 +28,16 @@ class ReleasePathCustomServiceTest { private GraphLoaderService graphLoaderService; @Autowired - private ReleasePathCustomService service; + private ReleasePathCustomService sut; + + @Test + void calculate_EmptyGraph_EmptyResult() { + given(graphLoaderService.loadGraph()) + .willReturn(new DefaultDirectedGraph<>(DefaultEdge.class)); + + Optional path = sut.getReleasePath(4L); + assertThat(path).isNotNull().isNotPresent(); + } @Test void getReleasePath_NodeOutsideGraph_EmptyPath() { @@ -40,8 +52,12 @@ void getReleasePath_NodeOutsideGraph_EmptyPath() { ) ); - Optional maybePath = service.getReleasePath(4L); - assertThat(maybePath).isEmpty(); + assertThatThrownBy(() -> sut.getReleasePath(4L)) + .isInstanceOf(MicroserviceNotFoundException.class) + .hasMessageStartingWith("Microservice not found") + .asInstanceOf(InstanceOfAssertFactories.type(MicroserviceNotFoundException.class)) + .extracting(MicroserviceNotFoundException::getMicroserviceId) + .isEqualTo(4L); } @Test @@ -66,33 +82,40 @@ void getReleasePath_NoCycles_Success() { ) ); - Optional maybePath = service.getReleasePath(1L); + Optional maybePath = sut.getReleasePath(1L); assertThat(maybePath).isPresent(); - ReleasePath path = maybePath.get(); + final ReleasePath path = maybePath.get(); - ReleaseStep step = getStep(path, 0, 5L); - assertThat(step).isNotNull(); - assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(4L, 7L); + final List groups = path.getGroups(); + + assertThat(groups).isNotEmpty().hasSize(5); - step = getStep(path, 0, 8L); - assertThat(step).isNotNull(); + ReleaseGroup group = groups.get(0); + assertThat(group.getSteps()).isNotEmpty().hasSize(2); + ReleaseStep step = group.findByTargetId(5L).orElseThrow(NoSuchElementException::new); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(4L, 7L); + step = group.findByTargetId(8L).orElseThrow(NoSuchElementException::new); assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(4L); - step = getStep(path, 1, 7L); - assertThat(step).isNotNull(); + group = groups.get(1); + assertThat(group.getSteps()).isNotEmpty().hasSize(1); + step = group.findByTargetId(7L).orElseThrow(NoSuchElementException::new); assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(4L); - step = getStep(path, 2, 4L); - assertThat(step).isNotNull(); + group = groups.get(2); + assertThat(group.getSteps()).isNotEmpty().hasSize(1); + step = group.findByTargetId(4L).orElseThrow(NoSuchElementException::new); assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(2L); - step = getStep(path, 3, 2L); - assertThat(step).isNotNull(); + group = groups.get(3); + assertThat(group.getSteps()).isNotEmpty().hasSize(1); + step = group.findByTargetId(2L).orElseThrow(NoSuchElementException::new); assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(1L); - step = getStep(path, 4, 1L); - assertThat(step).isNotNull(); + group = groups.get(4); + assertThat(group.getSteps()).isNotEmpty().hasSize(1); + step = group.findByTargetId(1L).orElseThrow(NoSuchElementException::new); assertThat(step.getParentWorkItems()).isEmpty(); } @@ -114,7 +137,7 @@ void getReleasePath_ContainsCyclesInSameComponent_ExceptionIsThrown() { assertThatIllegalArgumentException() .isThrownBy(() -> - service.getReleasePath(1L) + sut.getReleasePath(1L) ) .withMessageStartingWith("There are cyclic dependencies between microservices"); } @@ -137,55 +160,29 @@ void getReleasePath_ContainsCyclesInOtherComponent_Success() { ) ); - Optional maybePath = service.getReleasePath(1L); + Optional maybePath = sut.getReleasePath(1L); assertThat(maybePath).isPresent(); - ReleasePath path = maybePath.get(); + final ReleasePath path = maybePath.get(); + final List groups = path.getGroups(); - ReleaseStep step = getStep(path, 0, 3); - assertThat(step).isNotNull(); - assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(2L); + assertThat(groups).isNotEmpty().hasSize(3); - step = getStep(path, 0, 4); - assertThat(step).isNotNull(); + ReleaseGroup group = groups.get(0); + assertThat(group.getSteps()).isNotEmpty().hasSize(2); + ReleaseStep step = group.findByTargetId(3L).orElseThrow(NoSuchElementException::new); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(2L); + step = group.findByTargetId(4L).orElseThrow(NoSuchElementException::new); assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(2L); - step = getStep(path, 1, 2); - assertThat(step).isNotNull(); + group = groups.get(1); + assertThat(group.getSteps()).isNotEmpty().hasSize(1); + step = group.findByTargetId(2L).orElseThrow(NoSuchElementException::new); assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(1L); - step = getStep(path, 2, 1L); - assertThat(step).isNotNull(); + group = groups.get(2); + assertThat(group.getSteps()).isNotEmpty().hasSize(1); + step = group.findByTargetId(1L).orElseThrow(NoSuchElementException::new); assertThat(step.getParentWorkItems()).isEmpty(); } - - private ReleaseStep getStep(final ReleasePath path, int groupIndex, long microserviceId) { - final Set steps = getSteps(path, groupIndex); - if (steps != null) { - Optional maybeStep = steps.stream() - .filter(s -> s.getWorkItem() != null && microserviceId == s.getWorkItem().getId()).findFirst(); - if (maybeStep.isPresent()) { - return maybeStep.get(); - } - } - - return null; - } - - private Set getSteps(ReleasePath path, int groupIndex) { - if (path.getGroups() == null) { - return null; - } - - if (groupIndex > path.getGroups().size()) { - return null; - } - - ReleaseGroup first = path.getGroups().get(groupIndex); - if (first != null) { - return first.getSteps(); - } else { - return Collections.emptySet(); - } - } } diff --git a/src/test/java/com/github/microcatalog/web/rest/AuditResourceIT.java b/src/test/java/com/github/microcatalog/web/rest/AuditResourceIT.java index 41cd7ea..2ae135a 100644 --- a/src/test/java/com/github/microcatalog/web/rest/AuditResourceIT.java +++ b/src/test/java/com/github/microcatalog/web/rest/AuditResourceIT.java @@ -4,18 +4,14 @@ import com.github.microcatalog.domain.PersistentAuditEvent; import com.github.microcatalog.repository.PersistenceAuditEventRepository; import com.github.microcatalog.security.AuthoritiesConstants; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; @@ -32,7 +28,7 @@ @WithMockUser(authorities = AuthoritiesConstants.ADMIN) @SpringBootTest(classes = MicrocatalogApp.class) @Transactional -public class AuditResourceIT { +class AuditResourceIT { private static final String SAMPLE_PRINCIPAL = "SAMPLE_PRINCIPAL"; private static final String SAMPLE_TYPE = "SAMPLE_TYPE"; @@ -57,7 +53,7 @@ public void initTest() { } @Test - public void getAllAudits() throws Exception { + void getAllAudits() throws Exception { // Initialize the database auditEventRepository.save(auditEvent); @@ -69,7 +65,7 @@ public void getAllAudits() throws Exception { } @Test - public void getAudit() throws Exception { + void getAudit() throws Exception { // Initialize the database auditEventRepository.save(auditEvent); @@ -81,7 +77,7 @@ public void getAudit() throws Exception { } @Test - public void getAuditsByDate() throws Exception { + void getAuditsByDate() throws Exception { // Initialize the database auditEventRepository.save(auditEvent); @@ -90,19 +86,19 @@ public void getAuditsByDate() throws Exception { String toDate = SAMPLE_TIMESTAMP.plusSeconds(SECONDS_PER_DAY).toString().substring(0, 10); // Get the audit - restAuditMockMvc.perform(get("/management/audits?fromDate="+fromDate+"&toDate="+toDate)) + restAuditMockMvc.perform(get("/management/audits?fromDate=" + fromDate + "&toDate=" + toDate)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(jsonPath("$.[*].principal").value(hasItem(SAMPLE_PRINCIPAL))); } @Test - public void getNonExistingAuditsByDate() throws Exception { + void getNonExistingAuditsByDate() throws Exception { // Initialize the database auditEventRepository.save(auditEvent); // Generate dates for selecting audits by date, making sure the period will not contain the sample audit - String fromDate = SAMPLE_TIMESTAMP.minusSeconds(2*SECONDS_PER_DAY).toString().substring(0, 10); + String fromDate = SAMPLE_TIMESTAMP.minusSeconds(2 * SECONDS_PER_DAY).toString().substring(0, 10); String toDate = SAMPLE_TIMESTAMP.minusSeconds(SECONDS_PER_DAY).toString().substring(0, 10); // Query audits but expect no results @@ -113,14 +109,14 @@ public void getNonExistingAuditsByDate() throws Exception { } @Test - public void getNonExistingAudit() throws Exception { + void getNonExistingAudit() throws Exception { // Get the audit restAuditMockMvc.perform(get("/management/audits/{id}", Long.MAX_VALUE)) .andExpect(status().isNotFound()); } @Test - public void testPersistentAuditEventEquals() throws Exception { + void testPersistentAuditEventEquals() throws Exception { TestUtil.equalsVerifier(PersistentAuditEvent.class); PersistentAuditEvent auditEvent1 = new PersistentAuditEvent(); auditEvent1.setId(1L); diff --git a/src/test/java/com/github/microcatalog/web/rest/custom/ImpactAnalysisCustomResourceTest.java b/src/test/java/com/github/microcatalog/web/rest/custom/ImpactAnalysisCustomResourceTest.java new file mode 100644 index 0000000..928193a --- /dev/null +++ b/src/test/java/com/github/microcatalog/web/rest/custom/ImpactAnalysisCustomResourceTest.java @@ -0,0 +1,45 @@ +package com.github.microcatalog.web.rest.custom; + +import com.github.microcatalog.MockMvcWithUser; +import com.github.microcatalog.domain.custom.impact.analysis.Result; +import com.github.microcatalog.service.custom.ImpactAnalysisService; +import com.github.microcatalog.utils.MicroserviceBuilder; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(classes = ImpactAnalysisCustomResource.class) +@MockMvcWithUser +class ImpactAnalysisCustomResourceTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ImpactAnalysisService service; + + @Test + void calculateImpactAnalysisResult() throws Exception { + + given(service.calculate(3L)) + .willReturn(Optional.of( + new Result().target(new MicroserviceBuilder().withId(3L).build()))); + + mockMvc.perform(get("/api/impact-analysis/microservice/3")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.target.id").value(3L)); + } +} diff --git a/src/test/java/com/github/microcatalog/web/rest/custom/ReleasePathCustomResourceTest.java b/src/test/java/com/github/microcatalog/web/rest/custom/ReleasePathCustomResourceTest.java new file mode 100644 index 0000000..3e6fd4f --- /dev/null +++ b/src/test/java/com/github/microcatalog/web/rest/custom/ReleasePathCustomResourceTest.java @@ -0,0 +1,43 @@ +package com.github.microcatalog.web.rest.custom; + +import com.github.microcatalog.MockMvcWithUser; +import com.github.microcatalog.domain.custom.ReleasePath; +import com.github.microcatalog.service.custom.ReleasePathCustomService; +import com.github.microcatalog.utils.MicroserviceBuilder; +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.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(classes = ReleasePathCustomResource.class) +@MockMvcWithUser +class ReleasePathCustomResourceTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ReleasePathCustomService service; + + @Test + void getReleasePath() throws Exception { + + given(service.getReleasePath(3L)) + .willReturn(Optional.of( + new ReleasePath() + .target(new MicroserviceBuilder().withId(3L).build()))); + + mockMvc.perform(get("/api/release-path/microservice/3")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.target.id").value(3L)); + } +}