diff --git a/.gitignore b/.gitignore index a3b584a3..b70941ce 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ src/main/resources/static/favicon.ico src/main/resources/static/js/ src/main/resources/templates/index.html spy.log +docker/*.jar diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..c441ecff --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,58 @@ +pipeline { + agent any + + environment { + DOCKER_REPO = "taskagile/vuejs.spring-boot.mysql" + DOCKER_CREDENTIAL = "dockerhub" + JENKINS_AT_STAGING = "jenkins@staging.taskagile.com" + } + + stages { + stage("Build package") { + steps { + echo "Git commit: ${env.GIT_COMMIT}" + sh "mvn clean package" + } + } + + stage("Build Docker image") { + steps { + sh "cp target/app-0.0.1-SNAPSHOT.jar docker/app.jar" + sh "docker build -t ${DOCKER_REPO}:${env.GIT_COMMIT} docker/" + } + } + + stage("Push Docker build image") { + steps { + withDockerRegistry([ credentialsId: DOCKER_CREDENTIAL, url: '' ]) { + sh "docker push ${DOCKER_REPO}:${env.GIT_COMMIT}" + } + } + } + + stage("Deploy to staging") { + steps { + sh "ssh ${JENKINS_AT_STAGING} rm -fr /app/start.sh" + sh "scp ./docker/start.sh ${JENKINS_AT_STAGING}:/app" + sh "ssh ${JENKINS_AT_STAGING} \"cd /app && ./start.sh ${env.GIT_COMMIT}\"" + } + } + + stage("Run E2E tests") { + steps { + sh "cd ${env.WORKSPACE}/front-end && npm run test:staging-e2e" + } + } + } + + post { + always { + emailext ( + subject: "[Jenkins] ${env.JOB_NAME} Build #${env.BUILD_NUMBER} - ${currentBuild.currentResult}", + recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']], + body: "${currentBuild.currentResult}\n\nJob: ${env.JOB_NAME}\nBuild: #${env.BUILD_NUMBER}\nGit commit: ${env.GIT_COMMIT}\nMore detail at: ${env.BUILD_URL}" + ) + sh "docker rmi -f ${DOCKER_REPO}:${env.GIT_COMMIT}" + } + } +} diff --git a/README.md b/README.md index 2c2e5888..d6d51799 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,25 @@ Open source task management tool built with Vue.js 2, Spring Boot 2, and MySQL 5.7+ -## Local development +> This is the repository for the book [_Building applications with Spring 5 and Vue.js 2: A real-world practical guide to building a modern full-stack web application_](https://www.amazon.com/Building-applications-Spring-5-0-Vue-js-ebook/dp/B079X1VTST). -Create `src/main/resources/application-dev.properties` with the following settings to override the settings in `application.properties`. +## Local development setup + +### Prerequisites + +- JDK8 - OpenJDK Preferred +- MySQL 5.7+ +- RabbitMQ 3.6+ +- GraphicMagick 1.3+ + +### Database setup + +- Create database `task_agile` +- Initialize database with scripts in `setup` folder + +### Add dev properties file + +- Create `src/main/resources/application-dev.properties` with the following settings to override the settings in `application.properties`. ```properties spring.datasource.url=jdbc:mysql://localhost:3306/task_agile?useSSL=false @@ -14,8 +30,30 @@ spring.datasource.password= ## Commands -- Use `mvn install` to build both the front-end and the back-end - Use `mvn test` to run the tests of the back-end and the front-end - Use `mvn spring-boot:run` to start the back-end - Use `npm run serve` inside the `front-end` directory to start the front-end +- Use `mvn install` to build both the front-end and the back-end - Use `java -jar target/app-0.0.1-SNAPSHOT.jar` to start the bundled application + +## How to run application inside docker + +```bash +$ mvn clean package +$ cp target/app-0.0.1-SNAPSHOT.jar docker/app.jar +$ docker build -t taskagile:dev docker/ +``` + +### Start with dev profile locally + +```bash +$ docker run --rm --name taskagile -e "SPRING_PROFILES_ACTIVE=dev" -p 8080:8080 -p 9000:9000 taskagile +``` + +### Start on server + +With active profiles `staging` and `docker`. Make sure `docker` is the last one in the list so that the settings in `evn.list` will be applied. + +```bash +$ docker run --rm --name taskagile --env-file ./docker/env.list -e "SPRING_PROFILES_ACTIVE=staging,docker" -p 8080:8080 -p 9000:9000 taskagile +``` diff --git a/build.sh b/build.sh new file mode 100644 index 00000000..fce6c405 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +# Local build command include local e2e testing +mvn clean install -P local-e2e diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..34b60887 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,11 @@ +From openjdk:8-jre-alpine + +RUN apk add graphicsmagick=1.3.30-r0 +RUN ln -s /usr/bin/gm /usr/local/bin/gm + +ADD app.jar /opt/taskagile/app.jar +ADD application-docker.properties /config/application-docker.properties + +EXPOSE 8080 9000 + +ENTRYPOINT [ "java", "-jar", "/opt/taskagile/app.jar" ] diff --git a/docker/application-docker.properties b/docker/application-docker.properties new file mode 100644 index 00000000..661ab66c --- /dev/null +++ b/docker/application-docker.properties @@ -0,0 +1,11 @@ +spring.datasource.url=jdbc:mysql://${TASK_AGILE_DB_HOST}:3306/${TASK_AGILE_DB_NAME}?useSSL=false +spring.datasource.username=${TASK_AGILE_DB_USERNAME} +spring.datasource.password=${TASK_AGILE_DB_PASSWORD} + +spring.rabbitmq.host=${TASK_AGILE_MQ_HOST} +spring.rabbitmq.port=${TASK_AGILE_MQ_PORT} +spring.rabbitmq.username=${TASK_AGILE_MQ_USERNAME} +spring.rabbitmq.password=${TASK_AGILE_MQ_PASSWORD} + +spring.mail.host=${TASK_AGILE_MAIL_HOST} +spring.mail.port=${TASK_AGILE_MAIL_PORT} diff --git a/docker/env.list b/docker/env.list new file mode 100644 index 00000000..d6ba435e --- /dev/null +++ b/docker/env.list @@ -0,0 +1,16 @@ +TASK_AGILE_TOKEN_SECRET_KEY=60dKuW2Qpc3YkUoaa9i6qY5cyaGgQM8clfxpDGWS3sY= + +TASK_AGILE_DB_HOST= +TASK_AGILE_DB_NAME=task_agile +TASK_AGILE_DB_USERNAME= +TASK_AGILE_DB_PASSWORD= + +TASK_AGILE_MQ_HOST= +TASK_AGILE_MQ_PORT=5672 +TASK_AGILE_MQ_USERNAME= +TASK_AGILE_MQ_PASSWORD= + +TASK_AGILE_MAIL_HOST= +TASK_AGILE_MAIL_PORT=25 + +TASK_AGILE_CDN_URL= diff --git a/docker/start.sh b/docker/start.sh new file mode 100755 index 00000000..8323aa19 --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,5 @@ +docker pull taskagile/vuejs.spring-boot.mysql:$1 +docker container stop taskagile +docker run --detach --rm --name taskagile --env-file ./env.list \ + -e "SPRING_PROFILES_ACTIVE=staging,docker" \ + -p 8080:8080 -p 9000:9000 taskagile/vuejs.spring-boot.mysql:$1 diff --git a/front-end/package.json b/front-end/package.json index 794b1255..39006b1a 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -8,7 +8,8 @@ "lint": "vue-cli-service lint", "test:unit": "vue-cli-service test:unit", "test:e2e": "vue-cli-service test:e2e", - "test:integration": "vue-cli-service test:e2e --url http://localhost:8080/", + "test:staging-e2e": "vue-cli-service test:e2e --url https://staging.taskagile.com/", + "test:local-e2e": "vue-cli-service test:e2e --url http://localhost:8080/", "test": "npm run test:unit && npm run test:e2e" }, "dependencies": { diff --git a/front-end/tests/e2e/specs/0.register.e2e.js b/front-end/tests/e2e/specs/0.register.e2e.js index ec603af7..eda4ea5b 100644 --- a/front-end/tests/e2e/specs/0.register.e2e.js +++ b/front-end/tests/e2e/specs/0.register.e2e.js @@ -29,7 +29,7 @@ module.exports = { registerPage .navigate() - .waitForElementVisible('@app', 500) + .waitForElementVisible('@app', 30000) .assert.visible('@usernameInput') .assert.visible('@emailAddressInput') .assert.visible('@firstNameInput') diff --git a/front-end/tests/e2e/specs/1.login.e2e.js b/front-end/tests/e2e/specs/1.login.e2e.js index f715868d..c020bbf3 100644 --- a/front-end/tests/e2e/specs/1.login.e2e.js +++ b/front-end/tests/e2e/specs/1.login.e2e.js @@ -6,7 +6,7 @@ module.exports = { loginPage .navigate() - .waitForElementVisible('@app', 500) + .waitForElementVisible('@app', 30000) .assert.visible('@usernameInput') .assert.visible('@passwordInput') .assert.visible('@submitButton') @@ -20,7 +20,7 @@ module.exports = { .navigate() .login('not-exist', 'incorrect') - browser.pause(500) + browser.pause(2000) loginPage .assert.visible('@formError') diff --git a/pom.xml b/pom.xml index d923a68f..7f4af5d4 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ 3.7.0 1.11.409 1.2 + 1.6.0 @@ -153,6 +154,70 @@ + + + + local-e2e + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pre local-e2e test + + start + + + + + e2e + + + + + post local-e2e test + + stop + + + + + + + org.codehaus.mojo + exec-maven-plugin + ${codehaus.version} + + + front-end local-e2e test + + exec + + integration-test + + npm + + run + test:local-e2e + + + + + + ${basedir}/front-end + + + + + + + @@ -171,29 +236,12 @@ build-info - - pre integration test - - start - - - - e2e - - - - - post integration test - - stop - - org.codehaus.mojo exec-maven-plugin - 1.6.0 + ${codehaus.version} font-end install @@ -236,20 +284,6 @@ - - front-end e2e test - - exec - - integration-test - - npm - - run - test:integration - - - ${basedir}/front-end @@ -257,7 +291,6 @@ maven-resources-plugin - 3.1.0 copy front-end template @@ -335,7 +368,6 @@ maven-clean-plugin - 3.1.0 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2b25d01c..f2b7095d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -37,9 +37,6 @@ spring.mail.host=localhost spring.mail.port=1025 spring.mail.properties.mail.smtp.auth=false -logging.level.com.taskagile=DEBUG -logging.level.org.springframework.security=DEBUG - # Actuator setting management.server.port=9000 management.endpoint.health.show-details=always diff --git a/src/test/java/com/taskagile/web/apis/RegistrationApiControllerTests.java b/src/test/java/com/taskagile/web/apis/RegistrationApiControllerTests.java index aea8dbc1..020d8a74 100644 --- a/src/test/java/com/taskagile/web/apis/RegistrationApiControllerTests.java +++ b/src/test/java/com/taskagile/web/apis/RegistrationApiControllerTests.java @@ -12,6 +12,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; @@ -25,6 +26,7 @@ @RunWith(SpringRunner.class) @ContextConfiguration(classes = {SecurityConfiguration.class, RegistrationApiController.class}) @WebMvcTest +@ActiveProfiles("test") public class RegistrationApiControllerTests { @Autowired diff --git a/src/test/java/com/taskagile/web/apis/authenticate/AuthenticationFilterTests.java b/src/test/java/com/taskagile/web/apis/authenticate/AuthenticationFilterTests.java index ab8b205f..2561193a 100644 --- a/src/test/java/com/taskagile/web/apis/authenticate/AuthenticationFilterTests.java +++ b/src/test/java/com/taskagile/web/apis/authenticate/AuthenticationFilterTests.java @@ -8,6 +8,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -15,6 +16,7 @@ import static org.mockito.Mockito.verify; @RunWith(SpringRunner.class) +@ActiveProfiles("test") public class AuthenticationFilterTests { @MockBean diff --git a/src/test/java/integration/RegistrationApiIntegrationTests.java b/src/test/java/integration/RegistrationApiIntegrationTests.java new file mode 100644 index 00000000..2c307e4a --- /dev/null +++ b/src/test/java/integration/RegistrationApiIntegrationTests.java @@ -0,0 +1,90 @@ +package integration; + +import com.taskagile.TaskAgileApplication; +import com.taskagile.utils.JsonUtils; +import com.taskagile.web.payload.RegistrationPayload; +import org.junit.Test; +import org.junit.runner.RunWith; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = TaskAgileApplication.class) +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class RegistrationApiIntegrationTests { + + @Autowired + private MockMvc mvcMock; + + private RegistrationPayload payload(String username, String emailAddress) { + RegistrationPayload payload = new RegistrationPayload(); + payload.setUsername(username); + payload.setEmailAddress(emailAddress); + payload.setPassword("MyPassword!"); + payload.setFirstName("User"); + payload.setLastName("Test"); + return payload; + } + + @Test + public void register_blankPayload_shouldFailAndReturn400() throws Exception { + mvcMock.perform(post("/api/registrations")) + .andExpect(status().is(400)); + } + + @Test + public void register_validPayload_shouldSucceedAndReturn201() throws Exception { + RegistrationPayload payload = payload("sunny", "sunny@taskagile.com"); + mvcMock.perform( + post("/api/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(payload))) + .andExpect(status().is(201)); + } + + @Test + public void register_existedUsername_shouldFailAndReturn400() throws Exception { + RegistrationPayload payload = payload("exist", "test1@taskagile.com"); + mvcMock.perform( + post("/api/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(payload))) + .andExpect(status().is(201)); + // Try to register again with the same username + RegistrationPayload payload2 = payload("exist", "test2@taskagile.com"); + mvcMock.perform( + post("/api/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(payload2))) + .andExpect(status().is(400)) + .andExpect(jsonPath("$.message").value("Username already exists")); + } + + @Test + public void register_existedEmailAddress_shouldFailAndReturn400() throws Exception { + RegistrationPayload payload = payload("test1", "exist@taskagile.com"); + mvcMock.perform( + post("/api/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(payload))) + .andExpect(status().is(201)); + // Try to register with the same email address + RegistrationPayload payload2 = payload("test2", "exist@taskagile.com"); + mvcMock.perform( + post("/api/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(payload2))) + .andExpect(status().is(400)) + .andExpect(jsonPath("$.message").value("Email address already exists")); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index b79a2561..47a3a5a3 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -17,6 +17,8 @@ spring.jpa.open-in-view=false spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.freemarker.template-loader-path=classpath:/mail-templates/ + spring.mail.host=localhost spring.mail.port=1025 spring.mail.properties.mail.smtp.auth=false diff --git a/src/test/resources/mail-templates/welcome.ftl b/src/test/resources/mail-templates/welcome.ftl new file mode 100644 index 00000000..186a8581 --- /dev/null +++ b/src/test/resources/mail-templates/welcome.ftl @@ -0,0 +1,6 @@ +

Welcome!

+

Here is your registration information:

+
    +
  • Username: ${user.username}
  • +
  • Email Address: ${user.emailAddress}
  • +
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy