diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1199134..187afac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,31 +1,37 @@ -name: Software-Development-Simulation-Main-Build +name: Build and Analyze with SonarQube on: push: branches: - main + pull_request: + branches: + - main jobs: build: - name: Build and analyze runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 22 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: java-version: 22 + distribution: 'corretto' - name: Cache SonarQube packages - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar + - name: Pre-fetch Maven dependencies + run: mvn dependency:go-offline - name: Cache Maven packages - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} @@ -34,4 +40,4 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=Software-Development-Simulation -Dsonar.projectName='Software Development Simulation' -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml \ No newline at end of file + run: mvn clean install -B verify sonar:sonar \ No newline at end of file diff --git a/README.md b/README.md index 45db103..4990309 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![SonarQube Quality Gate](https://img.shields.io/badge/SonarQube%20Quality%20Gate-Passed-brightgreen) ![SonarQube Duplicated Lines](https://img.shields.io/badge/SonarQube%20Duplicated%20Lines-0%25-brightgreen) ![SonarQube LOC](https://img.shields.io/badge/SonarQube%20LOC-2000-blue) -![JaCoCo Coverage](https://img.shields.io/badge/JaCoCo%20Coverage-93.1%25-brightgreen) +![JaCoCo Coverage](https://img.shields.io/badge/JaCoCo%20Coverage-95.8%25-brightgreen) **Note:** SonarQube information is based on the last GitHub Action run and is generated locally. As such, there is no direct link available to the SonarQube dashboard. @@ -13,6 +13,23 @@ The Software Development Simulation project is a web-based application designed to simulate and manage software development tasks. It utilizes Spring Boot for backend services, Spring Integration for messaging, and integrates with Swagger for API documentation. +## Usage guide + +This application is to be used from web interface that has two views: + +1. Main view on index page, with fallowing features: + - Allows user input for desired number of randomly generated development Epics, which is given as range between MIN - MAX (to input exact number user should input same value for both). + - Allows saving those generated epics along with current development teams setup for future use, or manual change. + - Allows managing previously saved, i.e. predefined epics, and consequently user stories and technical tasks, with option to save without overriding previous data. + - Allows addition, editing and removal of epics, user stories and technical tasks in currently loaded predefined data. + - Provides view of informational output, error output as well as jira stream generated output during application flow. + - Provides options to change to "Developers page" view. +2. Developers page view, with fallowing features + - View of current development teams setups and details about each developer (their personal data, skill level, position, etc.). + - Allows managing of said data through editing developer data, adding, moving or removing individual developers from their development teams. + - Provide options to generate new batch of developers data with random data using predefined parameters (male/female gender ratio, MIN - MAX range of total possible number of developers and MIN - MAX possible number of developers assigned in each development team). + - Provides option to add previously mentioned generated developers data to existing or to create fresh data (by checking option to not retain previous development team setup) + ## Features - **Spring Boot Application**: Built with Spring Boot 3.3.5. diff --git a/pom.xml b/pom.xml index 79dcdb3..306b78d 100755 --- a/pom.xml +++ b/pom.xml @@ -37,11 +37,23 @@ UTF-8 - 3.3.5 - 6.3.5 - 1.49.2 - 5.11.3 + 3.3.5 + 6.3.5 + 1.55.0 + 5.11.3 + 5.14.2 ${project.version} + 0.8.12 + Software-Development-Simulation + Software Development Simulation + ${application.version} + ${env.SONAR_HOST_URL} + ${env.SONAR_TOKEN} + target/classes + + src/main/java/dev/markodojkic/softwaredevelopmentsimulation/SoftwareDevelopmentSimulationApp.java + + reuseReports @@ -49,38 +61,38 @@ org.springframework.boot spring-boot - ${spring-version} + ${spring.version} compile org.springframework.boot spring-boot-autoconfigure - ${spring-version} + ${spring.version} compile true org.springframework.boot spring-boot-starter-thymeleaf - ${spring-version} + ${spring.version} compile org.springframework.boot spring-boot-devtools - ${spring-version} + ${spring.version} true org.springframework.boot spring-boot-starter-web - ${spring-version} + ${spring.version} compile org.springframework.boot spring-boot-test-autoconfigure - ${spring-version} + ${spring.version} test @@ -88,25 +100,25 @@ org.springframework.integration spring-integration-core - ${spring-integration-version} + ${spring-integration.version} compile org.springframework.integration spring-integration-stream - ${spring-integration-version} + ${spring-integration.version} compile org.springframework.integration spring-integration-mqtt - ${spring-integration-version} + ${spring-integration.version} compile org.springframework.integration spring-integration-file - ${spring-integration-version} + ${spring-integration.version} compile @@ -159,37 +171,37 @@ com.github.hazendaz.jmockit jmockit - ${jmockit-version} + ${jmockit.version} test org.junit.jupiter junit-jupiter - ${junit-jupiter-version} + ${junit-jupiter.version} test org.junit.jupiter junit-jupiter-api - ${junit-jupiter-version} + ${junit-jupiter.version} test org.junit.jupiter junit-jupiter-engine - ${junit-jupiter-version} + ${junit-jupiter.version} test org.junit.platform - junit-platform-suite-api + junit-platform-suite 1.11.3 test org.mockito mockito-junit-jupiter - 5.14.2 + ${mockito.version} test @@ -230,7 +242,7 @@ org.springframework.boot spring-boot-maven-plugin - ${spring-version} + ${spring.version} dev.markodojkic.softwaredevelopmentsimulation.SoftwareDevelopmentSimulationApp JAR @@ -252,26 +264,27 @@ 22 + + org.sonarsource.scanner.maven + sonar-maven-plugin + 5.0.0.4389 + org.jacoco jacoco-maven-plugin - 0.8.12 + ${jacoco.version} prepare-agent - - - - report - test - report + test dev/markodojkic/softwaredevelopmentsimulation/SoftwareDevelopmentSimulationApp.class + ${project.build.directory}/site/jacoco/jacoco.xml @@ -282,18 +295,27 @@ 3.5.1 - ${argLine} -Dspring.profiles.active=test -Xshare:off -XX:+EnableDynamicAgentLoading - -javaagent:${user.home}/.m2/repository/com/github/hazendaz/jmockit/jmockit/${jmockit-version}/jmockit-${jmockit-version}.jar + -javaagent:${settings.localRepository}/com/github/hazendaz/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar + -javaagent:${settings.localRepository}/org/jacoco/org.jacoco.agent/${jacoco.version}/org.jacoco.agent-${jacoco.version}-runtime.jar=destfile=target/jacoco.exec + dev/markodojkic/softwaredevelopmentsimulation/test/SoftwareDevelopmentSimulationAppTestsSuite org.codehaus.mojo versions-maven-plugin 2.17.1 + + + + use-latest-versions + + + diff --git a/src/main/java/dev/markodojkic/softwaredevelopmentsimulation/model/BaseTask.java b/src/main/java/dev/markodojkic/softwaredevelopmentsimulation/model/BaseTask.java index e046619..5cbc6ac 100644 --- a/src/main/java/dev/markodojkic/softwaredevelopmentsimulation/model/BaseTask.java +++ b/src/main/java/dev/markodojkic/softwaredevelopmentsimulation/model/BaseTask.java @@ -40,10 +40,10 @@ public String toString() { } public void setAssignee(Developer assignee) { - this.assignee = this.assignee == null ? assignee : this.assignee; + this.assignee = assignee == null ? this.assignee : assignee; } public void setReporter(Developer reporter) { - this.reporter = this.reporter == null ? reporter : this.reporter; + this.reporter = reporter == null ? this.reporter : reporter; } } \ No newline at end of file diff --git a/src/main/java/dev/markodojkic/softwaredevelopmentsimulation/util/Utilities.java b/src/main/java/dev/markodojkic/softwaredevelopmentsimulation/util/Utilities.java index 2730503..db2af2a 100644 --- a/src/main/java/dev/markodojkic/softwaredevelopmentsimulation/util/Utilities.java +++ b/src/main/java/dev/markodojkic/softwaredevelopmentsimulation/util/Utilities.java @@ -66,12 +66,11 @@ public class Utilities { static { boolean isTesting = System.getProperty("spring.profiles.active", "default").equals("test"); - Path base = isTesting ? Paths.get("src/test/resources", "dev.markodojkic.software_development_simulation.testing_data") : Paths.get(System.getProperty("user.home"), "dev.markodojkic", "software_development_simulation", "1.4.0"); + currentApplicationDataPath = isTesting ? Paths.get("src/test/resources", "dev.markodojkic.software_development_simulation.testing_data") : Paths.get(System.getProperty("user.home"), "dev.markodojkic", "software_development_simulation", "1.4.0"); - currentApplicationDataPath = base; - currentApplicationLogsPath = Paths.get(String.valueOf(base), "logs", isTesting ? "2012-12-12 00-00-00" : ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss"))); + currentApplicationLogsPath = Paths.get(String.valueOf(currentApplicationDataPath), "logs", isTesting ? "2012-12-12 00-00-00" : ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss"))); - if(isTesting){ + if(!Files.exists(currentApplicationDataPath)){ try { Files.createDirectories(currentApplicationDataPath); Files.createDirectories(currentApplicationLogsPath); diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index 3e9a332..1a33a62 100644 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -16,5 +16,5 @@ |___/_|_| |_| |_|\__,_|_|\__,_|\__\___/|_| Application version: ${application.version} -Powered by Spring Boot (v${spring-version}) and Spring Integration (v${spring-integration-version}) +Powered by Spring Boot (v${spring.version}) and Spring Integration (v${spring-integration.version}) Ⓒ Marko Dojkić 2024 \ No newline at end of file diff --git a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/BaseTaskTest.java b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/BaseTaskTest.java index a8c25fa..c8a6d33 100644 --- a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/BaseTaskTest.java +++ b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/BaseTaskTest.java @@ -1,5 +1,6 @@ package dev.markodojkic.softwaredevelopmentsimulation.test; +import com.fasterxml.jackson.core.JsonProcessingException; import dev.markodojkic.softwaredevelopmentsimulation.enums.Priority; import dev.markodojkic.softwaredevelopmentsimulation.enums.DeveloperType; import dev.markodojkic.softwaredevelopmentsimulation.model.BaseTask; @@ -8,7 +9,9 @@ import org.junit.jupiter.api.Test; import java.time.ZonedDateTime; +import java.util.ArrayList; +import static dev.markodojkic.softwaredevelopmentsimulation.util.Utilities.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -25,6 +28,11 @@ void when_noArgsConstructorIsCalled_correctValuesAreSetAsDefault() { assertNull(task.getAssignee()); assertNull(task.getReporter()); assertNull(task.getCreatedOn()); + + // Assertion for toString method + String expectedToString = "BaseTask{id='null', name='null', description='null', priority=null, assignee='UNASSIGNED', reporter='UNASSIGNED', createdOn=null}"; + + assertEquals(expectedToString, task.toString()); } @Test @@ -82,6 +90,7 @@ void when_equalsOrHashCodeIsCalled_onEqualObjectAreSame_onNonEqualObjectsAreDiff // Test equality assertEquals(task1, task2); assertEquals(task1.hashCode(), task2.hashCode()); + assertNotEquals(task1, assignee1); // Create tasks with different IDs BaseTask task3 = new BaseTask("2", "Task Name", "Task Description", Priority.NORMAL, assignee1, reporter1, ZonedDateTime.now()); @@ -134,6 +143,15 @@ void when_gettersAndSettersAreCalled_valuesAreCorrectlyRetrievedOrSet() { assertEquals(Priority.CRITICAL, task.getPriority()); assertEquals("assignee 1", task.getAssignee().getDisplayName()); assertEquals("reporter 1", task.getReporter().getDisplayName()); + task.setAssignee(null); + task.setReporter(null); + assertEquals("assignee 1", task.getAssignee().getDisplayName()); + assertEquals("reporter 1", task.getReporter().getDisplayName()); assertNotNull(task.getCreatedOn()); } + + @Test + void when_emptyListIsPassedToJSONSerializer_emptyJSONArrayIsRetrieved() throws JsonProcessingException { + assertEquals("[]", getObjectMapper().writeValueAsString(new ArrayList<>())); + } } \ No newline at end of file diff --git a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/Config/SoftwareDevelopmentSimulationAppBaseTest.java b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/Config/SoftwareDevelopmentSimulationAppBaseTest.java index 3b61bb5..7e8e881 100644 --- a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/Config/SoftwareDevelopmentSimulationAppBaseTest.java +++ b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/Config/SoftwareDevelopmentSimulationAppBaseTest.java @@ -37,7 +37,6 @@ @SpringBootTest @AutoConfigureMockMvc(addFilters = false) // Enables MockMvc with full context @ContextConfiguration(classes = { MiscellaneousConfig.class, TestConfig.class, SpringIntegrationMessageChannelsConfig.class, MQTTFlow.class, PrintoutFlow.class, FileHandlingFlow.class, PrinterTransformer.class, DeveloperImpl.class, ProjectManagerImpl.class, MainController.class, DevelopersPageController.class }) -//@ExtendWith({MockitoExtension.class, GlobalSetupExtension.class}) public abstract class SoftwareDevelopmentSimulationAppBaseTest { private static Server mqttServer; diff --git a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/DeveloperTest.java b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/DeveloperTest.java index 08738a6..86286fc 100644 --- a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/DeveloperTest.java +++ b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/DeveloperTest.java @@ -44,7 +44,7 @@ void when_allArgsConstructorIsCalled_correctValuesAreSet() { } @Test - void testGetDisplayName() { + void when_getDisplayNameIsCalled_correctDisplayNameIsReturned() { developer.setName("Alice"); developer.setSurname("Johnson"); @@ -52,7 +52,7 @@ void testGetDisplayName() { } @Test - void testToString() { + void when_toStringIsCalled_correctDataIsReturned() { developer = new Developer("123", "Bob", "Smith", "9876543210987", "Test area", DeveloperType.INTERN_DEVELOPER, 1L, false); String expectedString = "Developer(id=123, name=Bob, surname=Smith, yugoslavianUMCN=9876543210987, placeOfBirth=Test area, developerType=INTERN_DEVELOPER, experienceCoefficient=1)"; diff --git a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/SoftwareDevelopmentSimulationAppTest.java b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/SoftwareDevelopmentSimulationAppTest.java index 6aaa65e..214d9d5 100644 --- a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/SoftwareDevelopmentSimulationAppTest.java +++ b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/SoftwareDevelopmentSimulationAppTest.java @@ -30,6 +30,7 @@ import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static dev.markodojkic.softwaredevelopmentsimulation.util.DataProvider.*; import static dev.markodojkic.softwaredevelopmentsimulation.util.Utilities.*; @@ -103,11 +104,10 @@ public Message preSend(Message message, MessageChannel channel) { @Test void when_generateRandomEpics_epicsAreCorrectlyCreated() throws Exception { - String originalOsName = System.getProperty("os.name"); - System.setProperty("os.name", "generic"); assertNotNull(epicMessageInput); assertNotNull(doneEpicsOutput); + updateDevelopmentTeamsSetup(new DevelopmentTeamCreationParameters(65, 30, 31, 15, 20, false)); CountDownLatch epicMessageInputLatch = new CountDownLatch(4); CountDownLatch epicMessageDoneLatch = new CountDownLatch(4); int epicCountDownLimit = 4; @@ -170,23 +170,32 @@ public Message preSend(Message message, MessageChannel channel) { assertFalse(Files.exists(getCurrentApplicationDataPath().resolve("predefinedData").resolve("2012-12-12 00-00-00").resolve("sessionData.json"))); assertFalse(Files.exists(getCurrentApplicationDataPath().resolve("predefinedData").resolve("2012-12-12 00-00-00").resolve("developersData.json"))); - - System.setProperty("os.name", originalOsName); } @Test void when_generateRandomEpicsWithSave_epicsAreCorrectlyCreatedAndSaved() throws Exception { Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS); - String originalOsName = System.getProperty("os.name"); - System.setProperty("os.name", "generic"); assertNotNull(doneEpicsOutput); - CountDownLatch epicMessageDoneLatch = new CountDownLatch(1); + CountDownLatch epicMessageDoneLatch; int epicCountDownLimit = 1; - int epicCountUpperLimit = 1; + int epicCountUpperLimit = 2; + AtomicInteger countOfEpics = new AtomicInteger(0); mockMvc.perform(post("/api/applicationFlowRandomized").param("save", "true").param("min", String.valueOf(epicCountDownLimit)).param("max", String.valueOf(epicCountUpperLimit))).andExpect(status().is2xxSuccessful()); + ((PriorityChannel) epicMessageInput).addInterceptor(new ExecutorChannelInterceptor() { + @Override + public Message preSend(Message message, MessageChannel channel) { + countOfEpics.incrementAndGet(); + return ExecutorChannelInterceptor.super.preSend(message, channel); + } + }); + + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + + epicMessageDoneLatch = new CountDownLatch(countOfEpics.get()); + ((DirectChannel) doneEpicsOutput).addInterceptor(new ExecutorChannelInterceptor() { @Override public Message preSend(Message message, MessageChannel channel) { @@ -206,8 +215,6 @@ public Message preSend(Message message, MessageChannel channel) { assertFalse(Files.readString(getCurrentApplicationLogsPath().resolve("jiraActivityStreamChannel.log")).isEmpty()); assertFalse(Files.readString(getCurrentApplicationDataPath().resolve("predefinedData").resolve("2012-12-12 00-00-00").resolve("sessionData.json")).isEmpty()); assertFalse(Files.readString(getCurrentApplicationDataPath().resolve("predefinedData").resolve("2012-12-12 00-00-00").resolve("developersData.json")).isEmpty()); - - System.setProperty("os.name", originalOsName); } @Test @@ -297,7 +304,10 @@ public Message preSend(Message message, MessageChannel channel) { .andExpect(status().is2xxSuccessful()) .andExpect(MockMvcResultMatchers.content().string("Data successfully saved to folder '2012-12-12 00-00-00'")); + assertEquals(Objects.requireNonNull(getObjectMapper().writeValueAsString(epicsInput)), Objects.requireNonNull(getObjectMapper().writeValueAsString(epicsDone))); //Direct test for epics custom json serializer/deserializer + assertEquals(Files.readString(Paths.get(Objects.requireNonNull(getClass().getClassLoader().getResource("testSessionData.json")).toURI())), Files.readString(getCurrentApplicationDataPath().resolve("predefinedData").resolve("2012-12-12 00-00-00").resolve("sessionData.json"))); + assertEquals(Files.readString(Paths.get(Objects.requireNonNull(getClass().getClassLoader().getResource("testDevelopersData.json")).toURI())), Files.readString(getCurrentApplicationDataPath().resolve("predefinedData").resolve("2012-12-12 00-00-00").resolve("developersData.json"))); } catch (Exception e) { fail(e.getCause()); diff --git a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/SwaggerTest.java b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/SwaggerTest.java index 8bc6db3..62c5407 100644 --- a/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/SwaggerTest.java +++ b/src/test/java/dev/markodojkic/softwaredevelopmentsimulation/test/SwaggerTest.java @@ -25,7 +25,7 @@ class SwaggerTest { private GroupedOpenApi groupedOpenApi; @Test - void testCustomOpenAPIConfig() { + void when_openAPIConfigIsCalled_correctDataIsReturned() { Info info = openAPI.getInfo(); assertEquals("Software development simulator™ API", info.getTitle()); assertEquals("This is the API documentation for the Software development simulator™ Developed by Ⓒ Marko Dojkić", info.getDescription()); @@ -41,7 +41,7 @@ void testCustomOpenAPIConfig() { } @Test - void testApiGroupConfig() { + void when_groupedOpenAPIConfigIsCalled_correctDataIsReturned() { // Verify that the GroupedOpenApi is set up with the correct group name and paths assertEquals("api", groupedOpenApi.getGroup()); assertEquals(List.of("/api/**"), groupedOpenApi.getPathsToMatch());