diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f87c340a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +FROM openjdk:11 +WORKDIR /app + +# get chromedriver& pipenv & unzip then delete cache +RUN apt-get update && apt-get install -y \ + chromium chromium-driver \ + python3 python3-pip pipenv \ + wget unzip xvfb fonts-liberation && \ + rm -rf /var/lib/apt/lists/* + +# Start Xvfb and set DISPLAY +ENV DISPLAY=:99 +# Set up Python environment +RUN pipenv install --python 3 \ + && pipenv install pytest requests behave behave2cucumber selenium PyHamcrest + + + +# gradle copy and set permission +COPY gradlew . +COPY gradle ./gradle +RUN chmod +x ./gradlew + +COPY build.gradle . + +COPY src ./src + +# build project +RUN ./gradlew clean build + +# install tomcat +RUN wget https://downloads.apache.org/tomcat/tomcat-9/v9.0.100/bin/apache-tomcat-9.0.100.tar.gz \ + && tar -xzf apache-tomcat-9.0.100.tar.gz \ + && mv apache-tomcat-9.0.100 tomcat \ + && rm apache-tomcat-9.0.100.tar.gz + + + +RUN mkdir -p tomcat/webapps/ + +# copy war file to tomcat webapps +RUN cp build/libs/app.war tomcat/webapps/demo.war + + + +EXPOSE 8080 + +# run tomcat server +CMD Xvfb :99 -screen 0 1920x1080x24 & \ + sh -c "./tomcat/bin/catalina.sh run" \ No newline at end of file diff --git a/GitWorkflow.png b/GitWorkflow.png new file mode 100644 index 00000000..d89da0e4 Binary files /dev/null and b/GitWorkflow.png differ diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..d37024fe --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,211 @@ +// This jenkinsfile is used to run CI/CD on my local (Windows) box, no VM's needed. + +pipeline { + + agent any + + environment { + // This is set so that the Python API tests will recognize it + // and go through the Zap proxy waiting at 9888 + HTTP_PROXY = 'http://127.0.0.1:9888' + } + + stages { + + // build the war file (the binary). This is the only + // place that happens. + stage('Build') { + steps { + sh './gradlew clean assemble' + } + } + + // run all the unit tests - these do not require anything else + // to be running and most run very quickly. + stage('Unit Tests') { + steps { + sh './gradlew test' + } + post { + always { + junit 'build/test-results/test/*.xml' + } + } + } + + // run the tests which require connection to a + // running database. + stage('Database Tests') { + steps { + sh './gradlew integrate' + } + post { + always { + junit 'build/test-results/integrate/*.xml' + } + } + } + + // These are the Behavior Driven Development (BDD) tests + // See the files in src/bdd_test + // These tests do not require a running system. + stage('BDD Tests') { + steps { + sh './gradlew generateCucumberReports' + // generate the code coverage report for jacoco + sh './gradlew jacocoTestReport' + } + post { + always { + junit 'build/test-results/bdd/*.xml' + } + } + } + + // Runs an analysis of the code, looking for any + // patterns that suggest potential bugs. + stage('Static Analysis') { + steps { + sh './gradlew sonarqube' + // wait for sonarqube to finish its analysis + sleep 5 + sh './gradlew checkQualityGate' + } + } + + + // Move the binary over to the test environment and + // get it running, in preparation for tests that + // require a whole system to be running. + stage('Deploy to Test') { + steps { + sh './gradlew deployToTestWindowsLocal' + // pipenv needs to be installed and on the path for this to work. + sh 'PIPENV_IGNORE_VIRTUALENVS=1 pipenv install' + + // Wait here until the server tells us it's up and listening + sh './gradlew waitForHeartBeat' + + // clear Zap's memory for the incoming tests + sh 'curl http://zap/JSON/core/action/newSession -s --proxy localhost:9888' + } + } + + + // Run the tests which investigate the functioning of the API. + stage('API Tests') { + steps { + sh './gradlew runApiTests' + } + post { + always { + junit 'build/test-results/api_tests/*.xml' + } + } + } + + // We use a BDD framework for some UI tests, Behave, because Python rules + // when it comes to experimentation with UI tests. You can try things and see how they work out. + // this set of BDD tests does require a running system. + // BDD at the UI level is just to ensure that basic capabilities work, + // not that every little detail of UI functionality is correct. For + // that purpose, see the following stage, "UI Tests" + stage('UI BDD Tests') { + steps { + sh './gradlew runBehaveTests' + sh './gradlew generateCucumberReport' + } + post { + always { + junit 'build/test-results/bdd_ui/*.xml' + } + } + } + + // This set of tests investigates the functionality of the UI. + // Note that this is separate fom the UI BDD Tests, which + // only focuses on essential capability and therefore only + // covers a small subset of the possibilities of UI behavior. + stage('UI Tests') { + steps { + sh 'cd src/ui_tests/java && ./gradlew clean test' + } + post { + always { + junit 'src/ui_tests/java/build/test-results/test/*.xml' + } + } + } + + // Run OWASP's "DependencyCheck". https://owasp.org/www-project-dependency-check/ + // You are what you eat - and so it is with software. This + // software consists of a number of software by other authors. + // For example, for this project we use language tools by Apache, + // password complexity analysis, and several others. Each one of + // these might have security bugs - and if they have a security + // bug, so do we! + // + // DependencyCheck looks at the list of known + // security vulnerabilities from the United States National Institute of + // Standards and Technology (NIST), and checks if the software + // we are importing has any major known vulnerabilities. If so, + // the build will halt at this point. + stage('Security: Dependency Analysis') { + steps { + sh './gradlew dependencyCheckAnalyze' + } + } + + // Run Jmeter performance testing https://jmeter.apache.org/ + // This test simulates 50 users concurrently using our software + // for a set of common tasks. + stage('Performance Tests') { + steps { + sh './gradlew runPerfTests' + } + } + + // Runs mutation testing against some subset of our software + // as a spot test. Mutation testing is where bugs are seeded + // into the software and the tests are run, and we see which + // tests fail and which pass, as a result. + // + // what *should* happen is that where code or tests are altered, + // the test should fail, shouldn't it? However, it sometimes + // happens that no matter how code is changed, the tests + // continue to pass, which implies that the test wasn't really + // providing any value for those lines. + stage('Mutation Tests') { + steps { + sh './gradlew pitest' + } + } + + stage('Build Documentation') { + steps { + sh './gradlew javadoc' + } + } + + stage('Collect Zap Security Report') { + steps { + sh 'mkdir -p build/reports/zap' + sh 'curl http://zap/OTHER/core/other/htmlreport --proxy localhost:9888 > build/reports/zap/zap_report.html' + } + } + + + // This is the stage where we deploy to production. If any test + // fails, we won't get here. Note that we aren't really doing anything - this + // is a token step, to indicate whether we would have deployed or not. Nothing actually + // happens, since this is a demo project. + stage('Deploy to Prod') { + steps { + // just a token operation while we pretend to deploy + sh 'sleep 5' + } + } + + } + +} diff --git a/Proejct_Instruction.md b/Proejct_Instruction.md new file mode 100644 index 00000000..f313ab56 --- /dev/null +++ b/Proejct_Instruction.md @@ -0,0 +1,58 @@ +# ENSF 400 - Winter 2025 - Course Project + +## Project Overview + +In this project, you will work based on a software project by incorporating/extending a complete CI/CD (Continuous Integration/Continuous Deployment) pipeline. This is based on an open-source sample application: https://github.com/7ep/demo + +This project can also be any application that requires the project of build, test, and deployment. +You will leverage GitHub for source control, Docker for containerizing your application, and a CI/CD tool (Jenkins) to automate the build, testing, and verification process. The goal is to validate every code change automatically through container builds, unit tests, code quality checks, and end-to-end functional tests. + + +## Project Requirements + +By the end of this project, your group must deliver the following: + +1. Manage your project on GitHub and follow proper Git workflows (branching, pull requests, code reviews). Document the process of how you use Git workflows to collaborate with your team members. + +1. Containerize your application for builds and deployments. Upload and download your container images to a public or private image repository (e.g., Docker Hub or GitHub Container Registry). Ensure a container image is built with unique build tag(s) matching the triggering commit from any branch. + +1. Set up an automated CI/CD with Jenkins in a Codespace environment. Configure the pipeline to trigger upon pull requests merging changes into the main branch. + +1. Document the CI/CD process and provide clear instructions on replicating your environment. Submit a video demo at the end of the project. + +### Existing Pipelines +You will also demonstrate the delivery of the following process and artifacts that come with the project. + +1. Run static analysis quality-gating using SonarQube +1. Performance testing with Jmeter +1. Security analysis with OWASP's "DependencyCheck" +1. Build Javadocs + + +## Evaluation Criteria + +Your project will be assessed on the following criteria: + +### GitHub Repository & Git Workflow (15%) +1. Project on GitHub in a public repository with all team members participating in the development and maintenance of the project (5%). +1. Demonstrate the process practicing Git workflows (branching, pull requests, code reviews) (10%). + +### Containerization (20%) +1. Dockerfile to containerize the project (5%). +1. Use of container image repository to upload and download images (5%). +1. Effective tagging mechanism for each building matching the commits/branches/pull requests (10%). + +### CI/CD Pipeline Automation (40%) +1. Jenkins integration with GitHub in Codespace (10%). +1. Triggering automated checks upon pull request to the main branch (10%). +1. Deployment process to automatically deploy the application in the Codespace environment upon a build (10%). +1. Be able to run items 5-8 in **Existing Pipelines** (10%). + +### Testing & Code Quality (10%) +1. Generate test coverage reports upon each automated build (5%). +1. Generate code quality report using SonarQube reports upon each automated build (5%). + +### Documentation & Demo (15%) +1. Clarity and completeness of README and other documentation. The documentation must demonstrate the team’s collaboration process (5%). +1. Demonstration video with a length not exceeding 10 minutes, showing a clear understanding of the pipeline and its benefits. The documentation must demonstrate the team’s collaboration process (10%). + diff --git a/build.gradle b/build.gradle index ff7b120e..708873ad 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,8 @@ plugins { // gretty is a gradle plugin to make it easy to run a server and hotswap code at runtime. // https://plugins.gradle.org/plugin/org.gretty - id 'org.gretty' version '3.0.4' + + id 'org.gretty' version '3.1.5' // provides access to a database versioning tool. id "org.flywaydb.flyway" version "6.0.8" @@ -219,7 +220,7 @@ cucumberReports { // merge together all the cucumber reports with a suffix of "json" reports = files(fileTree(dir: "build/bdd", include: '*.json')) testTasksFinalizedByReport = false - projectNameOverride = "$projectname" + projectNameOverride = 'demo-app' } flyway { diff --git a/docs/BDD_video.mp4 b/docs/BDD_video.mp4 index 27c4646c..571239ed 100644 Binary files a/docs/BDD_video.mp4 and b/docs/BDD_video.mp4 differ diff --git a/src/ui_tests/python/basic_test.py b/src/ui_tests/python/basic_test.py index 859f886f..9a28ca54 100644 --- a/src/ui_tests/python/basic_test.py +++ b/src/ui_tests/python/basic_test.py @@ -9,6 +9,7 @@ from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.select import Select from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.chrome.options import Options import requests from selenium.webdriver.common.proxy import Proxy, ProxyType from hamcrest import * @@ -23,7 +24,23 @@ class TestBasic(): def setup_class(self): - self.driver = webdriver.Chrome() + chrome_options = Options() + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--headless") + chrome_options.add_argument("--remote-debugging-port=9222") + chrome_options.add_argument("--disable-gpu") + chrome_options.add_argument("--disable-software-rasterizer") + chrome_options.add_argument("--disable-extensions") + chrome_options.add_argument("--disable-background-networking") + chrome_options.add_argument("--disable-background-timer-throttling") + chrome_options.add_argument("--disable-backgrounding-occluded-windows") + chrome_options.add_argument("--disable-breakpad") + chrome_options.add_argument("--disable-component-extensions-with-background-pages") + chrome_options.add_argument("--disable-features=TranslateUI,BlinkGenPropertyTrees") + chrome_options.add_argument("--disable-ipc-flooding-protection") + chrome_options.add_argument("--disable-renderer-backgrounding") + self.driver = webdriver.Chrome(options=chrome_options) self.vars = {} def teardown_class(self): @@ -86,15 +103,15 @@ def __init__(self, driver): self.driver = driver def enter_username(self, text): - login_username_field = self.driver.find_element_by_id("login_username") + login_username_field = self.driver.find_element(By.ID,"login_username") login_username_field.send_keys(text) def enter_password(self, text): - login_password_field = self.driver.find_element_by_id("login_password") + login_password_field = self.driver.find_element(By.ID,"login_password") login_password_field.send_keys(text) def enter(self): - login_button = self.driver.find_element_by_id("login_submit") + login_button = self.driver.find_element(By.ID,"login_submit") login_button.click() @@ -104,15 +121,15 @@ def __init__(self, driver): self.driver = driver def enter_username(self, text): - register_username_field = self.driver.find_element_by_id("register_username") + register_username_field = self.driver.find_element(By.ID,"register_username") register_username_field.send_keys(text) def enter_password(self, text): - register_password_field = self.driver.find_element_by_id("register_password") + register_password_field = self.driver.find_element(By.ID,"register_password") register_password_field.send_keys(text) def enter(self): - register_button = self.driver.find_element_by_id("register_submit") + register_button = self.driver.find_element(By.ID,"register_submit") register_button.click() @@ -122,11 +139,11 @@ def __init__(self, driver): self.driver = driver def register_book(self, text): - register_book_field = self.driver.find_element_by_id("register_book") + register_book_field = self.driver.find_element(By.ID,"register_book") register_book_field.send_keys(text) def enter(self): - register_button = self.driver.find_element_by_id("register_book_submit") + register_button = self.driver.find_element(By.ID,"register_book_submit") register_button.click() @@ -136,11 +153,11 @@ def __init__(self, driver): self.driver = driver def register_borrower(self, text): - register_borrower_field = self.driver.find_element_by_id("register_borrower") + register_borrower_field = self.driver.find_element(By.ID,"register_borrower") register_borrower_field.send_keys(text) def enter(self): - register_button = self.driver.find_element_by_id("register_borrower_submit") + register_button = self.driver.find_element(By.ID,"register_borrower_submit") register_button.click() @@ -150,15 +167,15 @@ def __init__(self, driver): self.driver = driver def enter_book(self, text): - book_field = self.driver.find_element_by_id("lend_book") + book_field = self.driver.find_element(By.ID,"lend_book") book_field.send_keys(text) def enter_borrower(self, text): - borrower_field = self.driver.find_element_by_id("lend_borrower") + borrower_field = self.driver.find_element(By.ID,"lend_borrower") borrower_field.send_keys(text) def enter(self): - lend_button = self.driver.find_element_by_id("lend_book_submit") + lend_button = self.driver.find_element(By.ID,"lend_book_submit") lend_button.click() @@ -168,15 +185,15 @@ def __init__(self, driver): self.driver = driver def enter_addend_a(self, text): - addend_a = self.driver.find_element_by_id("addend_a") + addend_a = self.driver.find_element(By.ID,"addend_a") addend_a.send_keys(text) def enter_addend_b(self, text): - addend_b = self.driver.find_element_by_id("addend_b") + addend_b = self.driver.find_element(By.ID,"addend_b") addend_b.send_keys(text) def enter(self): - lend_button = self.driver.find_element_by_id("math_submit") + lend_button = self.driver.find_element(By.ID,"math_submit") lend_button.click() # all the important capabilities for the Result page @@ -185,7 +202,7 @@ def __init__(self, driver): self.driver = driver def get_result_text(self): - return self.driver.find_element_by_id("result").text + return self.driver.find_element(By.ID,"result").text class LibraryPageObjectModel: