From c1ecf74373245287445a40fcae9b6541a633e636 Mon Sep 17 00:00:00 2001 From: Jacob See Date: Sun, 24 Jan 2021 15:29:35 -0800 Subject: [PATCH 1/7] Add 'entire file' tabs to first exercise --- exercises/1-the-manual-menace/README.md | 388 +++++++++++++++++++++++- exercises/index.html | 10 +- 2 files changed, 384 insertions(+), 14 deletions(-) diff --git a/exercises/1-the-manual-menace/README.md b/exercises/1-the-manual-menace/README.md index 621ac39b..5c22ce54 100644 --- a/exercises/1-the-manual-menace/README.md +++ b/exercises/1-the-manual-menace/README.md @@ -149,17 +149,42 @@ https:///f?url=https://raw.githubusercontent.com/rht-labs/enable 3. Open the `inventory/groups_vars/all.yml` file. Update the `namespace_prefix` variables by replacing the `` (including the `<` and `>`) with your name or initials. **Don't use uppercase or special characters**. For example; if your name is Tim Smith you would replace `` and set `namespace_prefix` to something like `tim` or `tsmith`. -πŸ“ _enablement-ci-cd/inventory/groups_vars/all.yml_ +πŸ“ enablement-ci-cd/inventory/groups_vars/all.yml + + + +#### ** Important Part ** ```yaml namespace_prefix: "" ``` +#### ** Entire File ** + +```yaml +--- +# Please change '' below to be unique for your deployment +# Note: +# - keep it lowercase +# - do NOT use special characters +# - make sure to replace the entire string between the double quotes - including the '<' and '>' + +namespace_prefix: "" # ⬅️ We care about this part! + +openshift_templates_raw: "https://raw.githubusercontent.com/rht-labs/openshift-templates" +openshift_templates_raw_version_tag: "v1.4.17" +cop_quickstarts: "https://github.com/redhat-cop/containers-quickstarts.git" +cop_quickstarts_raw: "https://raw.githubusercontent.com/redhat-cop/containers-quickstarts" +cop_quickstarts_raw_version_tag: "v1.29" +``` + + + 4. Open the `inventory/host_vars/projects-and-policies.yml` file; you should see some variables setup already to create the `-ci-cd` namespace. This object is passed to the OpenShift Applier to call the `templates/project-requests.yml` template with the parameters composed from the inventory and the `ci_cd` vars in the `apply.yml` playbook. We will add some additional content here but first let's explore the parameters and the template 5. Inside of the `inventory/host_vars/projects-and-policies.yml` you'll see the following -πŸ“ _enablement-ci-cd/inventory/host_vars/projects-and-policies.yml_ +πŸ‘€ enablement-ci-cd/inventory/host_vars/projects-and-policies.yml ```yaml ci_cd: @@ -169,7 +194,7 @@ ci_cd: - This will define the variables that we'll soon be using to deploy our CI/CD project. It relies on the `namespace_prefix` that we updated earlier. Pulling these two sets of variables together will now allow us to pass the newly created variables to our template that will create our project appropriately. You'll notice that the name of the variable above (`ci_cd`) is then assigned to `params_from_vars` in our inventory. -πŸ“ _enablement-ci-cd/inventory/host_vars/projects-and-policies.yml_ +πŸ‘€ enablement-ci-cd/inventory/host_vars/projects-and-policies.yml ```yaml ansible_connection: local @@ -188,7 +213,11 @@ openshift_cluster_content: - In your editor, open `enablement-ci-cd/inventory/host_vars/projects-and-policies.yml` and add the following lines before `openshift_cluster_content`: -πŸ“ _enablement-ci-cd/inventory/host_vars/projects-and-policies.yml_ +πŸ“ enablement-ci-cd/inventory/host_vars/projects-and-policies.yml + + + +#### ** Important Part ** ```yaml dev: @@ -200,9 +229,43 @@ test: NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Test" ``` +#### ** Entire File ** + +```yaml +--- +ci_cd: + NAMESPACE: "{{ namespace_prefix }}-ci-cd" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title}}s CI/CD" + +dev: + NAMESPACE: "{{ namespace_prefix }}-dev" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Dev" + +test: + NAMESPACE: "{{ namespace_prefix }}-test" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Test" + +ansible_connection: local +openshift_cluster_content: +- object: projectrequest + content: + - name: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/project-requests.yml" + action: create + params_from_vars: "{{ ci_cd }}" + tags: + - projects +``` + + + 7. In the `enablement-ci-cd/inventory/host_vars/projects-and-policies.yml` file, add the new objects for the projects you want to create (dev & test) by adding another object to the `content` array (previously defined) for each. You can copy and paste them from the `ci_cd_namespace` example and update them accordingly. If you do this, remember to set the names to `{{ dev_namespace }}` and `{{ test_namespace }}` and change the `params_from_vars` variable accordingly. The values for these variables used for the names (`ci_cd_namespace`, `dev_namespace` etc.) are defined in `apply.yml` file in the root of the project. -πŸ“ _enablement-ci-cd/inventory/host_vars/projects-and-policies.yml_ +πŸ“ enablement-ci-cd/inventory/host_vars/projects-and-policies.yml + + + +#### ** Important Part ** ```yaml - name: "{{ dev_namespace }}" @@ -219,6 +282,48 @@ test: - projects ``` +#### ** Entire File ** + +```yaml +--- +ci_cd: + NAMESPACE: "{{ namespace_prefix }}-ci-cd" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title}}s CI/CD" + +dev: + NAMESPACE: "{{ namespace_prefix }}-dev" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Dev" + +test: + NAMESPACE: "{{ namespace_prefix }}-test" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Test" + +ansible_connection: local +openshift_cluster_content: +- object: projectrequest + content: + - name: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/project-requests.yml" + action: create + params_from_vars: "{{ ci_cd }}" + tags: + - projects + - name: "{{ dev_namespace }}" + template: "{{ playbook_dir }}/templates/project-requests.yml" + action: create + params_from_vars: "{{ dev }}" + tags: + - projects + - name: "{{ test_namespace }}" + template: "{{ playbook_dir }}/templates/project-requests.yml" + action: create + params_from_vars: "{{ test }}" + tags: + - projects +``` + + + 8. Use the `Terminal > Open Terminal in specific container` menu item to open a terminal in the `node-rhel7-ansible` container ![open-terminal](../images/exercise1/open-terminal.png) @@ -291,18 +396,35 @@ touch params/nexus 2. The essential params to include in this file are: -πŸ“ _enablement-ci-cd/params/nexus_ +πŸ“ enablement-ci-cd/params/nexus + + + +#### ** Important Part ** + +``` +VOLUME_CAPACITY=5Gi +MEMORY_LIMIT=1Gi +``` + +#### ** Entire File ** ``` VOLUME_CAPACITY=5Gi MEMORY_LIMIT=1Gi ``` + + - You'll notice that this is different from how we defined our params for our projects. This is because there are multiple ways to do this. In cases like this, there may be a need to change some of these variables more frequently than others (i.e. giving the app more memory,etc.). In this case, it's easier to maintain them within their own separate params files. 3. Create a new object in the inventory variables `inventory/host_vars/ci-cd-tooling.yml` called `ci-cd-tooling` and populate its `content` as follows -πŸ“ _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ +πŸ“ enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** ```yaml --- @@ -320,6 +442,26 @@ openshift_cluster_content: - nexus ``` +#### ** Entire File ** + +```yaml +--- +ansible_connection: local +openshift_cluster_content: + - galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" + - object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus +``` + + + ![ci-cd-deployments-yml](../images/exercise1/ci-cd-deployments-yml.png)

@@ -372,7 +514,11 @@ git push -u origin --all 1. Open `enablement-ci-cd` in your favourite editor. Edit the `inventory/host_vars/ci-cd-tooling.yml` to include a new object for our mongodb as shown below. This item can be added below Nexus in the `ci-cd-tooling` section. -πŸ“ _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ +πŸ“ enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** ```yaml - name: "jenkins-mongodb" @@ -383,6 +529,33 @@ git push -u origin --all - mongodb ``` +#### ** Entire File ** + +```yaml +--- +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb +``` + + + ![jenkins-mongo](../images/exercise1/jenkins-mongo.png) 2. Git commit your updates to the inventory to git for traceability. @@ -423,7 +596,11 @@ ansible-playbook apply.yml -e target=tools \ touch params/jenkins ``` -πŸ“ _enablement-ci-cd/params/jenkins_ +πŸ“ enablement-ci-cd/params/jenkins + + + +#### ** Important Part ** ``` MEMORY_LIMIT=3Gi @@ -433,6 +610,18 @@ NAMESPACE=-ci-cd JENKINS_OPTS=--sessionTimeout=720 ``` +#### ** Entire File ** + +``` +MEMORY_LIMIT=3Gi +VOLUME_CAPACITY=15Gi +JVM_ARCH=x86_64 +NAMESPACE=-ci-cd +JENKINS_OPTS=--sessionTimeout=720 +``` + + + - You might be wondering why we have to replace here and can't just rely on the `namespace_prefix` variable that we've been using previously. This is because the replacement is handled by two different engines (one being ansible -- which knows about `namespace_prefix` and the other being the oc client, which does not). Because the params files are processed by the oc client, we need to update this here. 2. Add a `jenkins` variable to the Ansible inventory underneath the jenkins-mongo in `inventory/host_vars/ci-cd-tooling.yml` as shown below to create a DeploymentConfig for Jenkins. In order for Jenkins to be able to run `npm` commands we must configure a jenkins build agent for it to use. This agent will be dynamically provisioned when we run a build. It needs to have Node.js and npm and a C compiler installed in it. @@ -441,7 +630,11 @@ JENKINS_OPTS=--sessionTimeout=720 NOTE These agents can take a time to build themselves so to speed up we have placed the agent with a corresponding ImageStream within OpenShift. To leverage this existing agent image, we are using a feature of the openshift-applier to process a couple of post-steps part of the inventory. These steps are utilized to perform pre and post tasks necessary to make our inventory work correctly. In this case, we use the post steps to tag and label the jenkins-agent-npm ImageStream within our CI/CD project so Jenkins knows how to find and use it.

-πŸ“ _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ +πŸ“ enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** ```yaml - name: "jenkins" @@ -462,6 +655,49 @@ JENKINS_OPTS=--sessionTimeout=720 - jenkins ``` +#### ** Entire File ** + +```yaml +--- +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins0 +``` + + + This configuration, if applied now, will create the deployment configuration needed for Jenkins but the `${NAMESPACE}:${JENKINS_IMAGE_STREAM_TAG}` in the template won't exist yet. 3. To create this image we will take the supported OpenShift Container Platform Jenkins Image and bake in some extra configuration using an [s2i](https://github.com/openshift/source-to-image) builder image. More information on Jenkins s2i is found on the [openshift/jenkins](https://github.com/openshift/jenkins#installing-using-s2i-build) GitHub page. To create an s2i configuration for Jenkins, start with the pre-canned configuration source in the `enablement-ci-cd` repo (in the jenkins-s2i directory). @@ -498,7 +734,21 @@ slack:2.37 touch params/jenkins-s2i ``` -πŸ“ _enablement-ci-cd/params/jenkins-s2i_ +πŸ“ enablement-ci-cd/params/jenkins-s2i + + + +#### ** Important Part ** + +``` +SOURCE_REPOSITORY_URL= +NAME=jenkins +SOURCE_REPOSITORY_CONTEXT_DIR=jenkins-s2i +SOURCE_REPOSITORY_PASSWORD= +SOURCE_REPOSITORY_USERNAME= +``` + +#### ** Entire File ** ``` SOURCE_REPOSITORY_URL= @@ -508,28 +758,138 @@ SOURCE_REPOSITORY_PASSWORD= SOURCE_REPOSITORY_USERNAME= ``` + + where _ `` is the full clone path of the repo where this project is stored (including the https && .git) _ `` is the username builder pod will use to login and clone the repo with \* `` is the password the builder pod will use to authenticate and clone the repo using 6. At the top of `inventory/host_vars/ci-cd-tooling.yml` file underneath the `---`, add the following: -πŸ“ _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ +πŸ“ enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** ```yaml ci_cd: IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" ``` +#### ** Entire File ** + +```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +``` + + + 7. Create a new object `ci-cd-builds` in the Ansible `inventory/host_vars/ci-cd-tooling.yml` to drive the s2i build configuration.

⚑ NOTE ⚑ - We are using a custom jenkins template that works with latest version of OpenShift until the changes can be merged upstream.

-πŸ“ _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ + +πŸ“ enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** + +```yaml +- object: ci-cd-builds + content: + - name: "jenkins-s2i" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/jenkins-s2i-build-template-with-secret.yml" + params: "{{ playbook_dir }}/params/jenkins-s2i" + params_from_vars: "{{ ci_cd }}" + tags: + - jenkins +``` + +#### ** Entire File ** ```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins - object: ci-cd-builds content: - name: "jenkins-s2i" @@ -541,6 +901,8 @@ ci_cd: - jenkins ``` + + 8. Commit your code to your GitLab instance ```bash diff --git a/exercises/index.html b/exercises/index.html index 54a37887..bf240ed0 100644 --- a/exercises/index.html +++ b/exercises/index.html @@ -24,7 +24,14 @@ subMaxLevel: 5, auto2top: true, search: 'auto', - notFoundPage: 'notThe404.md' + notFoundPage: 'notThe404.md', + tabs: { + persist : false, + sync : false, + theme : 'classic', + tabComments: true, + tabHeadings: true + } } @@ -35,6 +42,7 @@ + From d1d7957f08cd9d9aef8fecf7629d789feb5f7c41 Mon Sep 17 00:00:00 2001 From: Jacob See Date: Sun, 24 Jan 2021 16:46:41 -0800 Subject: [PATCH 2/7] Add tabs to exercise 2 --- exercises/2-attack-of-the-pipelines/README.md | 230 +++++++++++++++++- 1 file changed, 226 insertions(+), 4 deletions(-) diff --git a/exercises/2-attack-of-the-pipelines/README.md b/exercises/2-attack-of-the-pipelines/README.md index 18d5b0e3..f8ea4366 100644 --- a/exercises/2-attack-of-the-pipelines/README.md +++ b/exercises/2-attack-of-the-pipelines/README.md @@ -135,7 +135,7 @@ Make sure that you are in your `-codeready` project while running this This updates the API endpoint in the `index.js` config file. Before you run the command, it will look like the following. -πŸ“ *todolist/src/config/index.js* +πŸ‘€ todolist/src/config/index.js ``` export default { todoEndpoint: "/api/todos" @@ -146,7 +146,9 @@ Afterwards, you should see something like this: ![fixApiUrl](../images/exercise2/black-magic.png) 8. The `todolist` has some scripts defined in the package.json at the root of the project. A snippet of the npm scripts are shown below. To run any of these scripts run `npm run `. -πŸ“ *todolist/package.json* + +πŸ‘€ todolist/package.json + ``` "scripts": { "serve": "vue-cli-service serve --open", @@ -345,15 +347,39 @@ with the following 2. Before we do this we need to change `` accordingly in the apply.yml file. -πŸ“ *todolist/.openshift-applier/apply.yml* +πŸ“ todolist/.openshift-applier/apply.yml + + + +#### ** Important Part ** + +```yaml +- name: Build and Deploy todolist + hosts: app + vars: + namespace_prefix: '' + ci_cd_namespace: '{{ namespace_prefix }}-ci-cd' ``` + +#### ** Entire File ** + +```yaml +--- - name: Build and Deploy todolist hosts: app vars: namespace_prefix: '' ci_cd_namespace: '{{ namespace_prefix }}-ci-cd' + dev_namespace: '{{ namespace_prefix }}-dev' + test_namespace: '{{ namespace_prefix }}-test' + tasks: + - include_role: + name: openshift-applier/roles/openshift-applier + ``` + + ![applier](../images/exercise2/applier.png) 3. With those changes in place we can now run the playbook. First install the `openshift-applier` dependency, using the `ansible-galaxy tool` as per exercise one and then run the playbook (from the todolist directory). This will populate the cluster with all the config needed for the front end app. @@ -561,7 +587,12 @@ Some of the key things to note: 2. The Jenkinsfile is mostly complete, however some minor changes will be needed to orchestrate namespaces. Find and replace all instances of `` in the Jenkinsfile. Update the `` to the one you log in to the cluster with; this variable is used in the namespace of your Git projects when checking out code etc. Replace `` with your Git domain (only the hostname, without `https://` or the repository name). -πŸ“ *todolist/Jenkinsfile* +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy // Jenkinsfile @@ -582,6 +613,197 @@ Some of the key things to note: } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + // sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + } +} + +``` + + + 3. With these changes in place, push your changes to the `develop` branch. ```bash git add Jenkinsfile From 544b6107b1e91eee41deb9b72b758768aa58bf44 Mon Sep 17 00:00:00 2001 From: Jacob See Date: Tue, 26 Jan 2021 00:31:30 -0800 Subject: [PATCH 3/7] Add tabs to exercise 4 --- exercises/4-an-enslaved-hope/README.md | 358 ++++++++++++++++++++++++- 1 file changed, 348 insertions(+), 10 deletions(-) diff --git a/exercises/4-an-enslaved-hope/README.md b/exercises/4-an-enslaved-hope/README.md index cab98221..860ce79c 100644 --- a/exercises/4-an-enslaved-hope/README.md +++ b/exercises/4-an-enslaved-hope/README.md @@ -62,7 +62,12 @@ _____ 3. The ZAP image we will use is pre-built and hosted on Quay. The Dockerfile used to built it can be found on the Red Hat Communities of Practice Containers Quickstarts repository, along with a host of other useful [Jenkins agents](https://github.com/redhat-cop/containers-quickstarts/tree/master/jenkins-agents). To save time, we will use a prebuilt image. As we did with our `jenkins-agent-npm`, let's add some `pre_steps` for the applier to pull this image into our cluster and label it for use in Jenkins. -πŸ“ *inventory/host_vars/ci-cd-tooling.yml* +πŸ“ inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** + ```yaml # JENKINS AGENTS - object: jenkins-agent-nodes @@ -83,6 +88,81 @@ _____ - jenkins-agents - zap-agent ``` + +#### ** Entire File ** + +```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +- object: ci-cd-builds + content: + - name: "jenkins-s2i" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/jenkins-s2i-build-template-with-secret.yml" + params: "{{ playbook_dir }}/params/jenkins-s2i" + params_from_vars: "{{ ci_cd }}" + tags: + - jenkins +# JENKINS AGENTS +- object: jenkins-agent-nodes + content: + - name: jenkins-agent-zap + namespace: "{{ ci_cd_namespace }}" + pre_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/jenkins-slave-zap:do500.v2" + img_tag: "jenkins-agent-zap:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-zap" + tags: + - jenkins-agents + - zap-agent + +``` + + + ![zap-object](../images/exercise4/zap-object.png) 4. Run the ansible playbook filtering with tag `zap` so only the zap build pods are run. @@ -107,21 +187,110 @@ ansible-playbook apply.yml -e target=tools \ 1. Create an object in `inventory/host_vars/ci-cd-tooling.yml` called `arachni` and add the following variables to tell your template where to find the agent definition to be built. -πŸ“ *inventory/host_vars/ci-cd-tooling.yml* +πŸ“ inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** + ```yaml +arachni: + SOURCE_REPOSITORY_URL: "{{ cop_quickstarts }}" + SOURCE_CONTEXT_DIR: jenkins-agents/jenkins-agent-arachni + BUILDER_IMAGE_NAME: quay.io/openshift/origin-jenkins-agent-base:4.5 + NAME: jenkins-agent-arachni + SOURCE_REPOSITORY_REF: "{{ cop_quickstarts_raw_version_tag }}" +``` - arachni: - SOURCE_REPOSITORY_URL: "{{ cop_quickstarts }}" - SOURCE_CONTEXT_DIR: jenkins-agents/jenkins-agent-arachni - BUILDER_IMAGE_NAME: quay.io/openshift/origin-jenkins-agent-base:4.5 - NAME: jenkins-agent-arachni - SOURCE_REPOSITORY_REF: "{{ cop_quickstarts_raw_version_tag }}" +#### ** Entire File ** + +```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +arachni: + SOURCE_REPOSITORY_URL: "{{ cop_quickstarts }}" + SOURCE_CONTEXT_DIR: jenkins-agents/jenkins-agent-arachni + BUILDER_IMAGE_NAME: quay.io/openshift/origin-jenkins-agent-base:4.5 + NAME: jenkins-agent-arachni + SOURCE_REPOSITORY_REF: "{{ cop_quickstarts_raw_version_tag }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +- object: ci-cd-builds + content: + - name: "jenkins-s2i" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/jenkins-s2i-build-template-with-secret.yml" + params: "{{ playbook_dir }}/params/jenkins-s2i" + params_from_vars: "{{ ci_cd }}" + tags: + - jenkins +- object: jenkins-agent-nodes + content: + - name: jenkins-agent-zap + namespace: "{{ ci_cd_namespace }}" + pre_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/jenkins-slave-zap:do500.v2" + img_tag: "jenkins-agent-zap:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-zap" + tags: + - jenkins-agents + - zap-agent ``` + + + ![arachni-object-parameters](../images/exercise4/arachni-object-parameters.png) 2. Add the definition below underneath the Zap config -πŸ“ *inventory/host_vars/ci-cd-tooling.yml* +πŸ“ inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** + ```yaml - name: jenkins-agent-arachni template: "{{ cop_quickstarts_raw }}/{{ cop_quickstarts_raw_version_tag }}/.openshift/templates/jenkins-agent-generic-template.yml" @@ -131,6 +300,93 @@ ansible-playbook apply.yml -e target=tools \ - jenkins-agents - arachni-agent ``` + +#### ** Entire File ** + +```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +arachni: + SOURCE_REPOSITORY_URL: "{{ cop_quickstarts }}" + SOURCE_CONTEXT_DIR: jenkins-agents/jenkins-agent-arachni + BUILDER_IMAGE_NAME: quay.io/openshift/origin-jenkins-agent-base:4.5 + NAME: jenkins-agent-arachni + SOURCE_REPOSITORY_REF: "{{ cop_quickstarts_raw_version_tag }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +- object: ci-cd-builds + content: + - name: "jenkins-s2i" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/jenkins-s2i-build-template-with-secret.yml" + params: "{{ playbook_dir }}/params/jenkins-s2i" + params_from_vars: "{{ ci_cd }}" + tags: + - jenkins +- object: jenkins-agent-nodes + content: + - name: jenkins-agent-zap + namespace: "{{ ci_cd_namespace }}" + pre_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/jenkins-slave-zap:do500.v2" + img_tag: "jenkins-agent-zap:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-zap" + tags: + - jenkins-agents + - zap-agent + - name: jenkins-agent-arachni + template: "{{ cop_quickstarts_raw }}/{{ cop_quickstarts_raw_version_tag }}/.openshift/templates/jenkins-agent-generic-template.yml" + params_from_vars: "{{ arachni }}" + namespace: "{{ ci_cd_namespace }}" + tags: + - jenkins-agents + - arachni-agent +``` + + + ![arachni-object](../images/exercise4/arachni-object.png) 3. Run the ansible playbook filtering with tag `arachni` so only the arachni build pods are run. @@ -175,7 +431,12 @@ NAME=todolist 4. Create a new object in `.openshift-applier/inventory/group_vars/all.yml` to drive the `ocp-pipeline` template with the parameters file you've just created. It can be put under the existing `todolist-build` object. -πŸ“ *.openshift-applier/inventory/group_vars/all.yml* +πŸ“ .openshift-applier/inventory/group_vars/all.yml + + + +#### ** Important Part ** + ```yaml - name: todolist-pipeline template: "{{ playbook_dir }}/templates/ocp-pipeline.yml" @@ -184,6 +445,83 @@ NAME=todolist tags: - pipeline ``` + +#### ** Entire File ** + +```yaml +--- +app_name: todolist + +build: + NAME: '{{ app_name }}' +deploy: + dev: + PIPELINES_NAMESPACE: '{{ ci_cd_namespace }}' + NAME: '{{ app_name }}' + NAMESPACE: '{{ dev_namespace }}' + DEPLOYER_USER: jenkins + NODE_ENV: dev + test: + PIPELINES_NAMESPACE: '{{ ci_cd_namespace }}' + NAME: '{{ app_name }}' + NAMESPACE: '{{ test_namespace }}' + DEPLOYER_USER: jenkins + NODE_ENV: test + +db: + MONGODB_DATABASE: '{{ app_name }}' + +openshift_cluster_content: + - object: app-builds + content: + - name: todolist-build + template: '{{ playbook_dir }}/templates/todolist-build.yml' + params_from_vars: '{{ build }}' + namespace: '{{ ci_cd_namespace }}' + tags: + - build + - name: todolist-pipeline + template: "{{ playbook_dir }}/templates/ocp-pipeline.yml" + params: "{{ playbook_dir }}/params/ocp-pipeline" + namespace: "{{ ci_cd_namespace }}" + tags: + - pipeline + - object: deploy-dev + content: + - name: todolist + template: '{{ playbook_dir }}/templates/todolist-deploy.yml' + params_from_vars: '{{ deploy.dev }}' + namespace: '{{ dev_namespace }}' + tags: + - deploy + - dev + - name: todolist-db + template: '{{ playbook_dir }}/templates/mongodb.yml' + params_from_vars: '{{ db }}' + namespace: '{{ dev_namespace }}' + tags: + - deploy + - dev + - object: deploy-test + content: + - name: todolist + template: '{{ playbook_dir }}/templates/todolist-deploy.yml' + params_from_vars: '{{ deploy.test }}' + namespace: '{{ test_namespace }}' + tags: + - deploy + - test + - name: todolist-db + template: '{{ playbook_dir }}/templates/mongodb.yml' + params_from_vars: '{{ db }}' + namespace: '{{ test_namespace }}' + tags: + - deploy + - test +``` + + + ![ocp-pipeline-applier](../images/exercise4/ocp-pipeline-applier.png) 5. Log in to OpenShift using the `oc` client, and use the OpenShift Applier to create the cluster content From 6781bef6fdbbd5a1cb7e2aed024e1b8d5980c7a1 Mon Sep 17 00:00:00 2001 From: Jacob See Date: Tue, 26 Jan 2021 20:53:43 -0800 Subject: [PATCH 4/7] Add tabs to exercise 3 --- .../README.md | 2412 ++++++++++++++++- 1 file changed, 2379 insertions(+), 33 deletions(-) diff --git a/exercises/3-revenge-of-the-automated-testing/README.md b/exercises/3-revenge-of-the-automated-testing/README.md index ce609a0f..99ae528f 100644 --- a/exercises/3-revenge-of-the-automated-testing/README.md +++ b/exercises/3-revenge-of-the-automated-testing/README.md @@ -135,8 +135,13 @@ npm run test:server 6. With our tests all passing in the cloud ide, let's add them to our pipeline. Open the `Jenkinsfile` in your editor and add the command to run all the tests in the `steps{}` part of the `node-build` stage. -πŸ“ todolist/Jenkinsfile -```Jenksfile +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + +```groovy steps { sh 'printenv' @@ -144,13 +149,209 @@ steps { sh 'npm install' echo '### Running tests ###' - sh 'npm run test:all:ci' + sh 'npm run test:all:ci +``` + +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + } +} + ``` + + 7. Running the tests is important, but so is reporting the results. In the `post{}` `always{}` section of the `Jenkinsfile` add the location for Jenkins for find the test reports -πŸ“ todolist/Jenkinsfile -```Jenksfile +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + +```groovy post { always { archive "**" @@ -160,6 +361,198 @@ post { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + } +} + +``` + + + 8. With this in place, commit the changes which should trigger a build. ```bash git add Jenkinsfile @@ -200,18 +593,224 @@ npm run e2e:ide 4. With tests executing successfully locally; let's add them to our Jenkins pipeline. To do this; we'll create a new stage in our `Jenkinsfile`. Create a new `stage` called `e2e test` to run after the `node-deploy stage` -πŸ“ todolist/Jenkinsfile -```Jenksfile +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + +```groovy stage("e2e test") { } ``` + +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + + } + } +} + +``` + + + ![e2e-stage-new](../images/exercise3/e2e-stage-new.png) 5. Set the agent that this stage should execute on. In this case it will use the same `jenkins-agent-npm` that was used in the build stage. Set the steps needed to execute the tests and add the reporting location -πŸ“ todolist/Jenkinsfile -```Jenksfile +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + +```groovy stage("e2e test") { agent { node { @@ -238,6 +837,222 @@ stage("e2e test") { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + } +} + +``` + + + 6. With this in place, commit the changes to trigger a build and enhance our pipeline ```bash git add Jenkinsfile @@ -298,7 +1113,7 @@ git push -u origin feature/important-flag 2. Navigate to the `server/api/todo/todo.spec.js` file. This contains all of the existing todo list api tests. These are broken down into simple `describe("api definition", function(){})` blocks which is BDD speak for how the component being tested should behave. Inside of each `it("should do something ", function(){})` statements we use some snappy language to illustrate the expected behaviour of the test. For example a `GET` request of the api is described and tested for the return to be of type Array as follows. -πŸ“ todolist/server/api/todo/todo.spec.js +πŸ‘€ todolist/server/api/todo/todo.spec.js ```javascript describe("GET /api/todos", function() { it("should respond with JSON array", function(done) { @@ -339,7 +1154,12 @@ npm run test:server * Check the `.expect()` clause is set to `.expect(200)` * Add a new test assertion to check that `res.body.important` is `true` below the `// YOUR TEST GO HERE` line. -πŸ“ todolist/server/api/todo/todo.spec.js +πŸ“ todolist/server/api/todo/todo.spec.js + + + +#### ** Important Part ** + ```javascript // Exercise 3 test case! it("should mark todo as important and persist it", function(done) { @@ -353,16 +1173,240 @@ it("should mark todo as important and persist it", function(done) { .expect(200) .expect("Content-Type", /json/) .end(function(err, res) { - if (err) return done(err); - res.body.should.have.property("_id"); - res.body.title.should.equal("LOVE endpoint/server side testing!"); - // YOUR TEST GO HERE - res.body.important.should.equal(true); - done(); + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("LOVE endpoint/server side testing!"); + // YOUR TEST GO HERE + res.body.important.should.equal(true); + done(); + }); +}); +``` + +#### ** Entire File ** + +```javascript +"use strict"; + +const app = require("../../app"); +const request = require("supertest"); +require("should"); + +describe("GET /api/todos", function() { + it("should respond with JSON array", function(done) { + request(app) + .get("/api/todos") + .expect(200) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body.should.be.instanceof(Array); + done(); + }); + }); +}); + +describe("POST /api/todos", function() { + it("should create the todo and return with the todo", function(done) { + request(app) + .post("/api/todos") + .send({ + title: "learn about endpoint/server side testing", + completed: false + }) + .expect(201) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("learn about endpoint/server side testing"); + res.body.completed.should.equal(false); + done(); + }); + }); +}); + +describe("GET /api/todos/:id", function() { + let todoId; + beforeEach(function createObjectToUpdate(done) { + request(app) + .post("/api/todos") + .send({ + title: "learn about endpoint/server side testing", + completed: false + }) + .expect(201) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + todoId = res.body._id; + done(); + }); + }); + it("should update the todo", function(done) { + request(app) + .get("/api/todos/" + todoId) + .expect(200) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body._id.should.equal(todoId); + res.body.title.should.equal("learn about endpoint/server side testing"); + res.body.completed.should.equal(false); + done(); + }); + }); + it("should return 404 for valid mongo object id that does not exist", function(done) { + request(app) + .get("/api/todos/" + "abcdef0123456789ABCDEF01") + .expect(404) + .end(function(err) { + if (err) return done(err); + done(); + }); + }); + it("should return 400 for invalid object ids", function(done) { + request(app) + .get("/api/todos/" + 123) + .expect(400) + .end(function(err, res) { + if (err) return done(err); + res.text.should.equal("not a valid mongo object id"); + done(); + }); + }); +}); + +describe("DELETE /api/todos/:id", function() { + let todoId; + beforeEach(function createObjectToUpdate(done) { + request(app) + .post("/api/todos") + .send({ + title: "learn about endpoint/server side testing", + completed: false + }) + .expect(201) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + todoId = res.body._id; + done(); + }); + }); + it("should delete the todo", function(done) { + request(app) + .delete("/api/todos/" + todoId) + .expect(204) + .end(function(err) { + if (err) return done(err); + done(); + }); + }); + it("should return 404 for valid mongo object id that does not exist", function(done) { + request(app) + .delete("/api/todos/" + "abcdef0123456789ABCDEF01") + .expect(404) + .end(function(err) { + if (err) return done(err); + done(); + }); + }); + it("should return 400 for invalid object ids", function(done) { + request(app) + .delete("/api/todos/" + 123) + .send({ title: "LOVE endpoint/server side testing!", completed: true }) + .expect(400) + .end(function(err, res) { + if (err) return done(err); + res.text.should.equal("not a valid mongo object id"); + done(); + }); + }); +}); + +describe("PUT /api/todos/:id", function() { + let todoId; + beforeEach(function createObjectToUpdate(done) { + request(app) + .post("/api/todos") + .send({ + title: "learn about endpoint/server side testing", + completed: false + }) + .expect(201) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + todoId = res.body._id; + done(); + }); + }); + it("should update the todo", function(done) { + request(app) + .put("/api/todos/" + todoId) + .send({ title: "LOVE endpoint/server side testing!", completed: true }) + .expect(200) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("LOVE endpoint/server side testing!"); + res.body.completed.should.equal(true); + done(); + }); + }); + it("should return 404 for valid mongo object id that does not exist", function(done) { + request(app) + .put("/api/todos/" + "abcdef0123456789ABCDEF01") + .send({ title: "LOVE endpoint/server side testing!", completed: true }) + .expect(404) + .end(function(err) { + if (err) return done(err); + done(); + }); + }); + it("should return 400 for invalid object ids", function(done) { + request(app) + .put("/api/todos/" + 123) + .send({ title: "LOVE endpoint/server side testing!", completed: true }) + .expect(400) + .end(function(err, res) { + if (err) return done(err); + res.text.should.equal("not a valid mongo object id"); + done(); + }); + }); + + + + // Exercise 3 test case! + it("should ....", function(done) { + request(app) + .put("/api/todos/" + todoId) + .send({ + title: "LOVE endpoint/server side testing!", + completed: true, + important: true + }) + .expect(200) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("LOVE endpoint/server side testing!"); + // YOUR TEST GO HERE + res.body.important.should.equal(true); + done(); }); + }); + }); + ``` + + 6. Run your test. It should fail. ```bash npm run test:server @@ -372,7 +1416,12 @@ npm run test:server 7. With our test now failing; let's implement the feature. This is quite a simple change - we first need to update the `server/api/todo/todo.model.js`. Add an additional property on the schema called `important` and make its type Boolean. -πŸ“ todolist/server/api/todo/todo.model.js +πŸ“ todolist/server/api/todo/todo.model.js + + + +#### ** Important Part ** + ```javascript const TodoSchema = new Schema({ title: String, @@ -381,8 +1430,66 @@ const TodoSchema = new Schema({ }); ``` +#### ** Entire File ** + +```javascript +'use strict'; + +const mongoose = require('mongoose'), + Schema = mongoose.Schema; + +const TodoSchema = new Schema({ + title: String, + completed: Boolean, + important: Boolean +}); + +module.exports = mongoose.model('Todo', TodoSchema); +``` + + + 8. Next we need to update the `server/config/seed.js` file so that the pre-generated todos have an important property. Add `important: false` below `completed: *` for each object. Don't forget to add a comma at the end of the `completed: *` line. +πŸ“ todolist/server/config/seed.js + + + +#### ** Important Part ** + +```javascript +completed: false, +important: false +``` + +#### ** Entire File ** + +```javascript +/** + * Populate DB with sample data on server start + * to disable, edit config/environment/index.js, and set `seedDB: false` + */ + +'use strict'; + +const Todo = require('../api/todo/todo.model'); + +Todo.find({}).remove(function() { + Todo.create({ + title : 'Learn some stuff about MongoDB', + completed: false, + important: false + }, { + title : 'Play with NodeJS', + completed: true, + important: false + }); +}); + +``` + + + ![api-add-seed-important](../images/exercise3/api-add-seed-important.png) 9. With your changes to the Database schema updated; re-run your tests. The tests should pass. @@ -464,8 +1571,78 @@ npm run test:client -- --watch 6. Let's implement the first test `it("should render a button with important flag"`. This test will assert if the button is present on the page and it contains the `.important-flag` CSS class. To implement this; add the `expect` statement as follows below the `// TODO - test goes here!` comment. -πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js +πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js + + + +#### ** Important Part ** + +```javascript + it("should render a button with important flag", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".important-flag").exists()).toBe(true); + }); +``` + +#### ** Entire File ** + ```javascript +/* eslint-disable */ +import { shallow, mount, createLocalVue } from "@vue/test-utils"; +import Vuex from "vuex"; +import TodoItem from "@/components/TodoItem.vue"; +// import { expect } from 'chai' + +import * as all from "../setup.js"; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +const todoItem = { + title: "Love Front End testing :)", + completed: true +}; + +describe("TodoItem.vue", () => { + it("has the expected html structure", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + // expect(wrapper.element).toMatchSnapshot(); + }); + + it("Renders title as 'Love Front End testing :)'", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.title).toMatch("Love Front End testing :)"); + }); + + it("Renders completed as true", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.completed).toEqual(true); + }); +}); + +let importantTodo; +let methods; + +describe("Important Flag button ", () => { + beforeEach(() => { + importantTodo = { + title: "Love Front End testing :)", + completed: true, + important: true + }; + methods = { markImportant: jest.fn() }; + }); + it("should render a button with important flag", () => { const wrapper = mount(TodoItem, { propsData: { todoItem: importantTodo } @@ -473,16 +1650,124 @@ npm run test:client -- --watch // TODO - test goes here! expect(wrapper.find(".important-flag").exists()).toBe(true); }); + it("should set the colour to red when true", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + }); + it("should set the colour to not red when false", () => { + importantTodo.important = false; + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + }); + it("call markImportant when clicked", () => { + const wrapper = mount(TodoItem, { + methods, + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + }); +}); ``` + + 7. Save the file. Observe that the test case has started failing because we have not yet implemented the feature! ![todoitem-fail-test](../images/exercise3/todoitem-fail-test.png) 8. With a basic assertion in place, let's continue on to the next few tests. We want the important flag to be red when an item in the todolist is marked accordingly. Conversely we want it to be not red when false. Let's create a check for `.red-flag` CSS property to be present when important is true and not when false. Complete the `expect` statements in your test file as shown below for both tests. -πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js +πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js + + + +#### ** Important Part ** + +```javascript + it("should set the colour to red when true", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".red-flag").exists()).toBe(true); + }); + it("should set the colour to not red when false", () => { + importantTodo.important = false; + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".red-flag").exists()).toBe(false); + }); +``` + +#### ** Entire File ** + ```javascript +/* eslint-disable */ +import { shallow, mount, createLocalVue } from "@vue/test-utils"; +import Vuex from "vuex"; +import TodoItem from "@/components/TodoItem.vue"; +// import { expect } from 'chai' + +import * as all from "../setup.js"; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +const todoItem = { + title: "Love Front End testing :)", + completed: true +}; + +describe("TodoItem.vue", () => { + it("has the expected html structure", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + // expect(wrapper.element).toMatchSnapshot(); + }); + + it("Renders title as 'Love Front End testing :)'", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.title).toMatch("Love Front End testing :)"); + }); + + it("Renders completed as true", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.completed).toEqual(true); + }); +}); + +let importantTodo; +let methods; + +describe("Important Flag button ", () => { + beforeEach(() => { + importantTodo = { + title: "Love Front End testing :)", + completed: true, + important: true + }; + methods = { markImportant: jest.fn() }; + }); + + it("should render a button with important flag", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".important-flag").exists()).toBe(true); + }); it("should set the colour to red when true", () => { const wrapper = mount(TodoItem, { propsData: { todoItem: importantTodo } @@ -498,12 +1783,119 @@ npm run test:client -- --watch // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(false); }); + it("call markImportant when clicked", () => { + const wrapper = mount(TodoItem, { + methods, + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + }); +}); + ``` + + 9. Finally, we want to make the flag clickable and for it to call a function to update the state. The final test in the `TodoItem.spec.js` we want to create should simulate this behaviour. Implement the `it("call markImportant when clicked", () ` test by first simulating the click of our important-flag and asserting the function `markImportant()` to write is executed. -πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js + +πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js + + + +#### ** Important Part ** + +```javascript + it("call markImportant when clicked", () => { + const wrapper = mount(TodoItem, { + methods, + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + const input = wrapper.find(".important-flag"); + input.trigger("click"); + expect(methods.markImportant).toHaveBeenCalled(); + }); +``` + +#### ** Entire File ** + ```javascript +/* eslint-disable */ +import { shallow, mount, createLocalVue } from "@vue/test-utils"; +import Vuex from "vuex"; +import TodoItem from "@/components/TodoItem.vue"; +// import { expect } from 'chai' + +import * as all from "../setup.js"; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +const todoItem = { + title: "Love Front End testing :)", + completed: true +}; + +describe("TodoItem.vue", () => { + it("has the expected html structure", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + // expect(wrapper.element).toMatchSnapshot(); + }); + + it("Renders title as 'Love Front End testing :)'", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.title).toMatch("Love Front End testing :)"); + }); + + it("Renders completed as true", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.completed).toEqual(true); + }); +}); + +let importantTodo; +let methods; + +describe("Important Flag button ", () => { + beforeEach(() => { + importantTodo = { + title: "Love Front End testing :)", + completed: true, + important: true + }; + methods = { markImportant: jest.fn() }; + }); + + it("should render a button with important flag", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".important-flag").exists()).toBe(true); + }); + it("should set the colour to red when true", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".red-flag").exists()).toBe(true); + }); + it("should set the colour to not red when false", () => { + importantTodo.important = false; + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".red-flag").exists()).toBe(false); + }); it("call markImportant when clicked", () => { const wrapper = mount(TodoItem, { methods, @@ -514,8 +1906,12 @@ npm run test:client -- --watch input.trigger("click"); expect(methods.markImportant).toHaveBeenCalled(); }); +}); + ``` + + 10. With our tests written for the feature's UI component, let's implement our code to pass the tests. Explore the `src/components/TodoItem.vue`. Each vue file is broken down into 3 sections * The `` contains the HTML of our component. This could include references to other Components also @@ -524,7 +1920,12 @@ npm run test:client -- --watch 11. Underneath the `` tag, let's add a new md-button. Add an `.important-flag` class on the `md-button` and put the svg of the flag provided inside it. -πŸ“ todolist/src/components/TodoItem.vue +πŸ“ todolist/src/components/TodoItem.vue + + + +#### ** Important Part ** + ```html @@ -533,6 +1934,87 @@ npm run test:client -- --watch ``` +#### ** Entire File ** + +```html + + + + + + +``` + + + 12. We should now see the first of our failing tests has started to pass. Running the app locally (using `npm run serve`) should show the flag appear in the UI. It is clickable but won't fire any events and the colour is not red as per our requirement.

@@ -541,16 +2023,107 @@ npm run test:client -- --watch Let's continue to implement the colour change for the flag. On our `` tag, add some logic to bind the css to the property of a `todo.important` by adding ` :class="{'red-flag': todoItem.important}" `. This logic will apply the CSS class when `todo.important` is true. -πŸ“ todolist/src/components/TodoItem.vue +πŸ“ todolist/src/components/TodoItem.vue + + + +#### ** Important Part ** + ```html ``` +#### ** Entire File ** + +```html + + + + + + +``` + + + 13. More tests should now be passing. Let's wire the click of the flag to an event in Javascript. In the methods section of the `` tags in the Vue file, implement the `markImportant()`. We want to wire this to the action to updateTodo, just like we have in the `markCompleted()` call above it. We also need to pass an additional property to this method called `important` -πŸ“ todolist/src/components/TodoItem.vue +πŸ“ todolist/src/components/TodoItem.vue + + + +#### ** Important Part ** + ```javascript markImportant() { // TODO - FILL THIS OUT IN THE EXERCISE @@ -559,9 +2132,97 @@ Let's continue to implement the colour change for the flag. On our `` tag, } ``` +#### ** Entire File ** + +```html + + + + + + +``` + + + 14. Let's connect the click button in the DOM to the Javascript function we've just created. In the template, add a click handler to the md-button to call the function `markImportant()` by adding ` @click="markImportant()"` to the `` tag -πŸ“ todolist/src/components/TodoItem.vue +πŸ“ todolist/src/components/TodoItem.vue + + + +#### ** Important Part ** + ```html @@ -569,6 +2230,89 @@ Let's continue to implement the colour change for the flag. On our `` tag, ``` +#### ** Entire File ** + +```html + + + + + + +``` + + + 15. Finally - we need to make it so that when a new todo item is created it will have an important property. Head to `src/store/actions.js` and add `important: false` below `completed: false` in the `addTodo(){}` action. ![fe-add-actions-important](../images/exercise3/fe-add-actions-important.jpg) @@ -595,7 +2339,12 @@ npm run serve:all 19. We need to implement the `actions` and `mutations` for our feature. Let's start with the tests. Open the `tests/unit/javascript/actions.spec.js` and navigate to the bottom of the file. Our action should should commit the `MARK_TODO_IMPORTANT` to the mutations. Scroll to the end of the test file and implement the skeleton test by adding `expect(commit.firstCall.args[0]).toBe("MARK_TODO_IMPORTANT");` as the assertion. -πŸ“ todolist/tests/unit/javascript/actions.spec.js +πŸ“ todolist/tests/unit/javascript/actions.spec.js + + + +#### ** Important Part ** + ```javascript it("should call MARK_TODO_IMPORTANT", done => { const commit = sinon.spy(); @@ -608,9 +2357,160 @@ npm run serve:all }); ``` +#### ** Entire File ** + +```javascript +import actions from "@/store/actions"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import sinon from "sinon"; +import config from "../../../src/config"; + +const todos = [ + { _id: 1, title: "learn testing", completed: true }, + { _id: 2, title: "learn testing 2", completed: false } +]; +let state; + +describe("loadTodos", () => { + beforeEach(() => { + let mock = new MockAdapter(axios); + mock.onGet(config.todoEndpoint).reply(200, todos); + }); + it("should call commit to the mutation function twice", done => { + const commit = sinon.spy(); + actions.loadTodos({ commit }).then(() => { + // console.log(commit) + expect(commit.calledTwice).toBe(true); + done(); + }); + }); + + it("should first call SET_LOADING", done => { + const commit = sinon.spy(); + actions.loadTodos({ commit }).then(() => { + // console.log(commit.firstCall.args[0]) + expect(commit.firstCall.args[0]).toBe("SET_TODOS"); + done(); + }); + }); + it("should second call SET_TODOS", done => { + const commit = sinon.spy(); + actions.loadTodos({ commit }).then(() => { + // console.log(commit) + expect(commit.secondCall.args[0]).toBe("SET_LOADING"); + done(); + }); + }); +}); + +describe("addTodos", () => { + beforeEach(() => { + state = {}; + let mock = new MockAdapter(axios); + // mock.onPost(/http:\/\/localhost:9000\/api\/todos\/.*/, {}) + mock.onPost(config.todoEndpoint).reply(200, todos); + }); + it("should call commit to the mutation function once", done => { + const commit = sinon.spy(); + state.newTodo = "Learn some mocking"; + actions.addTodo({ commit, state }).then(() => { + // console.log(commit) + expect(commit.calledOnce).toBe(true); + done(); + }); + }); + it("should first call ADD_TODO", done => { + const commit = sinon.spy(); + state.newTodo = "Learn some mocking"; + actions.addTodo({ commit, state }).then(() => { + // console.log(commit.firstCall.args[0]) + expect(commit.firstCall.args[0]).toBe("ADD_TODO"); + done(); + }); + }); +}); + +describe("setNewTodo", () => { + it("should call SET_NEW_TODO", () => { + const commit = sinon.spy(); + actions.setNewTodo({ commit, todo: "learn stuff about mockin" }); + expect(commit.firstCall.args[0]).toBe("SET_NEW_TODO"); + }); +}); + +describe("clearNewTodo", () => { + it("should call CLEAR_NEW_TODO", () => { + const commit = sinon.spy(); + actions.clearNewTodo({ commit }); + expect(commit.firstCall.args[0]).toBe("CLEAR_NEW_TODO"); + }); +}); + +describe("clearTodos", () => { + it("should call CLEAR_ALL_TODOS when all is true", () => { + const commit = sinon.spy(); + state.todos = todos; + actions.clearTodos({ commit, state }, true); + expect(commit.firstCall.args[0]).toBe("CLEAR_ALL_TODOS"); + }); + + it("should call CLEAR_ALL_DONE_TODOS when all is not passed", () => { + const commit = sinon.spy(); + state.todos = todos; + actions.clearTodos({ commit, state }); + expect(commit.firstCall.args[0]).toBe("CLEAR_ALL_DONE_TODOS"); + }); +}); + +describe("updateTodo", () => { + beforeEach(() => { + state = {}; + let mock = new MockAdapter(axios); + mock.onPut(`${config.todoEndpoint}/1`).reply(200, todos); + }); + it("should call commit to the mutation function once", done => { + const commit = sinon.spy(); + state.todos = todos; + actions.updateTodo({ commit, state }, { id: 1 }).then(() => { + expect(commit.calledOnce).toBe(true); + done(); + }); + }); + it("should call MARK_TODO_COMPLETED", done => { + const commit = sinon.spy(); + state.todos = todos; + actions.updateTodo({ commit, state }, { id: 1 }).then(() => { + // console.log(commit.firstCall.args[0]) + expect(commit.firstCall.args[0]).toBe("MARK_TODO_COMPLETED"); + done(); + }); + }); + it("should call MARK_TODO_IMPORTANT", done => { + const commit = sinon.spy(); + state.todos = todos; + actions + .updateTodo({ commit, state }, { id: 1, important: true }) + .then(() => { + // TODO - test goes here! + expect(commit.firstCall.args[0]).toBe("MARK_TODO_IMPORTANT"); + done(); + }); + }); +}); + +``` + + + 20. We should now have more failing tests, let's fix this by adding the call from our action to the mutation method. Open the `src/store/actions.js` file and scroll to the bottom to the `updateTodo()` method. Complete the if block by adding `commit("MARK_TODO_IMPORTANT", i);` as shown below. -πŸ“ todolist/src/store/actions.js +πŸ“ todolist/src/store/actions.js + + + +#### ** Important Part ** + ```javascript updateTodo({ commit, state }, { id, important }) { let i = state.todos.findIndex(todo => todo._id === id); @@ -622,10 +2522,244 @@ updateTodo({ commit, state }, { id, important }) { } ``` +#### ** Entire File ** + +```javascript +import axios from "axios"; +import config from "@/config"; + +const dummyData = [ + { + _id: 0, + title: "Learn awesome things about Labs πŸ”¬", + completed: false, + important: false + }, + { + _id: 1, + title: "Learn about my friend Jenkins πŸŽ‰", + completed: true, + important: false + }, + { + _id: 2, + title: "Drink Coffee β˜•πŸ’©", + completed: false, + important: true + } +]; +export default { + loadTodos({ commit }) { + return axios + .get(config.todoEndpoint) + .then(r => r.data) + .then(todos => { + commit("SET_TODOS", todos); + commit("SET_LOADING", false); + }) + .catch(err => { + if (err) { + console.info("INFO - setting dummy data because of ", err); + commit("SET_TODOS", dummyData); + commit("SET_LOADING", false); + } + }); + }, + addTodo({ commit, state }) { + if (!state.newTodo) { + // do not add empty todos + return; + } + // debugger + const todo = { + title: state.newTodo, + completed: false + }; + // console.info("TESTINT BLAH BLAH ", todo); + return axios + .post(config.todoEndpoint, todo) + .then(mongoTodo => { + commit("ADD_TODO", mongoTodo.data); + }) + .catch(err => { + if (err) { + console.info("INFO - Adding dummy todo because of ", err); + let mongoTodo = todo; + mongoTodo._id = "fake-todo-item-" + Math.random(); + commit("ADD_TODO", mongoTodo); + } + }); + }, + setNewTodo({ commit }, todo) { + // debugger + commit("SET_NEW_TODO", todo); + }, + clearNewTodo({ commit }) { + commit("CLEAR_NEW_TODO"); + }, + clearTodos({ commit, state }, all) { + // 1 fire and forget or + const deleteStuff = id => { + axios.delete(config.todoEndpoint + "/" + id).then(data => { + console.info("INFO - item " + id + " deleted", data); + }); + }; + + if (all) { + state.todos.map(todo => { + deleteStuff(todo._id); + }); + commit("CLEAR_ALL_TODOS"); + } else { + state.todos.map(todo => { + // axios remove all done by the id + if (todo.completed) { + deleteStuff(todo._id); + } + }); + commit("CLEAR_ALL_DONE_TODOS"); + } + // 2 return array of promises and resolve all + }, + /* eslint: ignore */ + updateTodo({ commit, state }, { id, important }) { + let i = state.todos.findIndex(todo => todo._id === id); + if (important) { + // TODO - add commit imporant here! + commit("MARK_TODO_IMPORTANT", i); + } else { + commit("MARK_TODO_COMPLETED", i); + } + // Fire and forget style backend update ;) + return axios + .put(config.todoEndpoint + "/" + state.todos[i]._id, state.todos[i]) + .then(() => { + console.info("INFO - item " + id + " updated"); + }); + } +}; + +``` + + + 21. Finally, let's implement the `mutation` for our feature. Again, starting with the tests... Open the `tests/unit/javascript/mutations.spec.js` to find our skeleton tests at the bottom of the file. Our mutation method is responsible for toggling the todo's `important` property between `true` and `false`. Let's implement the tests for this functionality by setting important to be true and calling the method expecting the inverse. Then let's set it to false and call the method expecting the inverse. Add the expectations below the `// TODO - test goes here!` comment as done previously. -πŸ“ todolist/tests/unit/javascript/mutations.spec.js +πŸ“ todolist/tests/unit/javascript/mutations.spec.js + + + +#### ** Important Part ** + +```javascript + it("it should MARK_TODO_IMPORTANT as false", () => { + state.todos = importantTodos; + // TODO - test goes here! + mutations.MARK_TODO_IMPORTANT(state, 0); + expect(state.todos[0].important).toBe(false); + }); + + it("it should MARK_TODO_IMPORTANT as true", () => { + state.todos = importantTodos; + // TODO - test goes here! + state.todos[0].important = false; + mutations.MARK_TODO_IMPORTANT(state, 0); + expect(state.todos[0].important).toBe(true); + }); +``` + +#### ** Entire File ** + ```javascript +import mutations from "@/store/mutations"; + +let state; +const todo = { + completed: true, + title: "testing sucks" +}; +const newTodo = "biscuits"; +const doneTodos = [ + { + completed: true, + title: "testing sucks" + }, + { + completed: false, + title: "easy testing is fun" + } +]; +const importantTodos = [ + { + completed: true, + title: "testing sucks", + important: true + } +]; + +describe("Mutation tests", () => { + beforeEach(() => { + state = {}; + }); + it("sets the loading to true", () => { + mutations.SET_LOADING(state, true); + expect(state.loading).toBe(true); + }); + it("sets the loading to false", () => { + mutations.SET_LOADING(state, false); + expect(state.loading).toBe(false); + }); + + it("sets all SET_TODOS", () => { + mutations.SET_TODOS(state, [todo]); + expect(state.todos.length).toBe(1); + }); + + it("SET_NEW_TODO", () => { + mutations.SET_NEW_TODO(state, newTodo); + expect(state.newTodo).toEqual(newTodo); + }); + + it("ADD_TODO", () => { + state.todos = []; + mutations.ADD_TODO(state, todo); + expect(state.todos.length).toBe(1); + }); + + it("CLEAR_NEW_TODO", () => { + state.newTodo = newTodo; + mutations.CLEAR_NEW_TODO(state, newTodo); + expect(state.newTodo).toEqual(""); + }); + + it("CLEAR_NEW_TODO", () => { + state.newTodo = newTodo; + mutations.CLEAR_NEW_TODO(state); + expect(state.newTodo).toEqual(""); + }); + + it("CLEAR_ALL_DONE_TODOS", () => { + state.todos = doneTodos; + mutations.CLEAR_ALL_DONE_TODOS(state); + expect(state.todos.length).toBe(1); + expect(state.todos[0].completed).toBe(false); + }); + + it("CLEAR_ALL_TODOS", () => { + state.todos = doneTodos; + mutations.CLEAR_ALL_TODOS(state); + expect(state.todos.length).toBe(0); + }); + + it("MARK_TODO_COMPLETED", () => { + state.todos = doneTodos; + mutations.MARK_TODO_COMPLETED(state, 0); + expect(state.todos[0].completed).toBe(false); + // check the reversy! + mutations.MARK_TODO_COMPLETED(state, 0); + expect(state.todos[0].completed).toBe(true); + }); + it("it should MARK_TODO_IMPORTANT as false", () => { state.todos = importantTodos; // TODO - test goes here! @@ -640,20 +2774,74 @@ updateTodo({ commit, state }, { id, important }) { mutations.MARK_TODO_IMPORTANT(state, 0); expect(state.todos[0].important).toBe(true); }); +}); + ``` + + 22. With our tests running and failing, let's implement the feature to their spec. Open the `src/store/mutations.js` and add another function called `MARK_TODO_IMPORTANT` below the `MARK_TODO_COMPLETED` to toggle `todo.important` between true and false. NOTE - add a `,` at the end of the `MARK_TODO_COMPLETED(){}` function call. -πŸ“ todolist/src/store/mutations.js +πŸ“ todolist/src/store/mutations.js + + + +#### ** Important Part ** + +```javascript + MARK_TODO_IMPORTANT(state, index) { + console.log("INFO - MARK_TODO_IMPORTANT"); + state.todos[index].important = !state.todos[index].important; + } +``` + +#### ** Entire File ** + ```javascript +export default { + SET_LOADING(state, bool) { + console.log("INFO - Setting loading wheel"); + state.loading = bool; + }, + SET_TODOS(state, todos) { + console.log("INFO - Setting todos"); + state.todos = todos; + }, + SET_NEW_TODO(state, todo) { + console.log("INFO - Setting new todo"); + state.newTodo = todo; + }, + ADD_TODO(state, todo) { + console.log("INFO - Add todo", todo); + state.todos.push(todo); + }, + CLEAR_NEW_TODO(state) { + console.log("INFO - Clearing new todo"); + state.newTodo = ""; + }, + CLEAR_ALL_DONE_TODOS(state) { + console.log("INFO - Clearing all done todos"); + state.todos = state.todos.filter(obj => obj.completed === false); + }, + CLEAR_ALL_TODOS(state) { + console.log("INFO - Clearing all todos"); + state.todos = []; + }, + MARK_TODO_COMPLETED(state, index) { + console.log("INFO - MARK_TODO_COMPLETED"); + state.todos[index].completed = !state.todos[index].completed; + }, MARK_TODO_IMPORTANT(state, index) { console.log("INFO - MARK_TODO_IMPORTANT"); state.todos[index].important = !state.todos[index].important; } +}; ``` + + ![mark-todo-important](../images/exercise3/mark-todo-important.png) 23. All our tests should now be passing. On the watch tab where they are running, hit `a` to re-run all tests and update any snapshots with `u` if needed. @@ -697,7 +2885,8 @@ touch tests/e2e/specs/importantFlag.js 2. Open this new file in your code editor and set out the initial blank template for an e2e test as below: -πŸ“ todolist/tests/e2e/specs/importantFlag.js +πŸ“ todolist/tests/e2e/specs/importantFlag.js + ```javascript module.exports = { "Testing important flag setting": browser => { @@ -705,11 +2894,38 @@ touch tests/e2e/specs/importantFlag.js } }; ``` + ![if-e2e-step1](../images/exercise3/if-e2e-step1.png) 3. Now get the test to access the todos page and wait for it to load. The url can be taken from `process.env.VUE_DEV_SERVER_URL` - ![if-e2e-step2](../images/exercise3/if-e2e-step2.png) +πŸ“ todolist/tests/e2e/specs/importantFlag.js + + + +#### ** Important Part ** + +```javascript +browser + .url(process.env.VUE_DEV_SERVER_URL + '/#/todo') + .waitForElementVisible('body', 5000); +``` + +#### ** Entire File ** + +```javascript +module.exports = { + "Testing important flag setting": browser => { + browser + .url(process.env.VUE_DEV_SERVER_URL + '/#/todo') + .waitForElementVisible('body', 5000); + } +}; +``` + + + +![if-e2e-step2](../images/exercise3/if-e2e-step2.png) 4. Write code to do the following: * Click the clear all button and then enter a value in the textbox to create a new item. @@ -722,7 +2938,12 @@ touch tests/e2e/specs/importantFlag.js ![if-e2e-step3a](../images/exercise3/if-e2e-step3a.png) -πŸ“ src/components/XofYItems.vue +πŸ“ src/components/XofYItems.vue + + + +#### ** Important Part ** + ```html ``` +#### ** Entire File ** + +```html + + + + + + + + + + + + +``` + + + 6. Write the following test code. The pauses allow for the body of the page to render the todo list before exercising the test code: -πŸ“ todolist/tests/e2e/specs/importantFlag.js +πŸ“ todolist/tests/e2e/specs/importantFlag.js + + + +#### ** Important Part ** + +```javascript +browser + .url(process.env.VUE_DEV_SERVER_URL + '/#/todo') + .waitForElementVisible("body", 5000) + .pause(5000) + .click('#clear-all') + .pause(2000) + .setValue('input',['set a todo',browser.Keys.ENTER]) + .pause(2000) + .assert.elementPresent(".important-flag") + .assert.elementNotPresent(".red-flag") + .click('.important-flag') + .end(); +``` + +#### ** Entire File ** + ```javascript module.exports = { "Testing important flag setting": browser => { @@ -757,6 +3101,8 @@ touch tests/e2e/specs/importantFlag.js }; ``` + + 7. Your final E2E test should look like the following: ![if-e2e-step4](../images/exercise3/e2e-code-listing-full.jpg) From 671428c8f07becb347dedff4dfd09d3e2c9e2c37 Mon Sep 17 00:00:00 2001 From: Jacob See Date: Tue, 26 Jan 2021 21:51:44 -0800 Subject: [PATCH 5/7] Add tabs to exercise 5 --- .../5-non-functionals-strike-back/README.md | 1945 ++++++++++++++++- 1 file changed, 1937 insertions(+), 8 deletions(-) diff --git a/exercises/5-non-functionals-strike-back/README.md b/exercises/5-non-functionals-strike-back/README.md index 8d59a255..77d4eef6 100644 --- a/exercises/5-non-functionals-strike-back/README.md +++ b/exercises/5-non-functionals-strike-back/README.md @@ -74,7 +74,12 @@ _____ 3. Create a new stage called `Security Scan` underneath the `stage("e2e test") { }` section as shown below. This is a parallel stage which will allow us to define additional `stages() {}` inside of it. We will add two stages in there, one for `Zap` and one for `Arachni`. The contents of the `e2e test` have been removed for simplicity. -πŸ“ *todolist/Jenkinsfile* +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage("e2e test") { // ... stuff in here .... @@ -91,9 +96,239 @@ _____ } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + + } + stage('Arachni') { + + } + } + } + } +} +``` + + + 4. Let's start filling out the configuration for the OWASP Zap scan first. We will set the label to our agent created in previous exercise and a `when` condition to only execute the job when on either the master or develop branch. -πŸ“ *todolist/Jenkinsfile* +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage('OWASP Scan') { agent { @@ -107,9 +342,246 @@ stage('OWASP Scan') { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + } + stage('Arachni') { + + } + } + } + } +} +``` + + + 5. Add a `step` with a `sh` command to run the tool by passing in the URL of the app we're going to test. -πŸ“ *todolist/Jenkinsfile* +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage('OWASP Scan') { agent { @@ -130,9 +602,254 @@ stage('OWASP Scan') { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + } + stage('Arachni') { + + } + } + } + } +} + +``` + + + 6. Finally add the reporting for Jenkins in `post` hook of our Declarative Pipeline. This is to report the findings of the scan in Jenkins as an HTML report. -πŸ“ *todolist/Jenkinsfile* +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage('OWASP Scan') { agent { @@ -166,9 +883,266 @@ stage('OWASP Scan') { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + post { + always { + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '', + reportFiles: 'index.html', + reportName: 'Zap Branniscan' + ] + } + } + } + stage('Arachni') { + + } + } + } + } +} +``` + + + 7. Let's add our Arachni Scan to the second part of the parallel block. The main difference between these sections is Jenkins will report an XML report too for failing the build accordingly. Below is the snippet for the Arachni scanning. -πŸ“ *todolist/Jenkinsfile* +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage('Arachni') { agent { @@ -202,6 +1176,285 @@ stage('OWASP Scan') { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + post { + always { + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '', + reportFiles: 'index.html', + reportName: 'Zap Branniscan' + ] + } + } + } + stage('Arachni') { + agent { + node { + label "jenkins-agent-arachni" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + /arachni/bin/arachni https://${E2E_TEST_ROUTE} --report-save-path=arachni-report.afr + /arachni/bin/arachni_reporter arachni-report.afr --reporter=xunit:outfile=report.xml --reporter=html:outfile=web-report.zip + unzip web-report.zip -d arachni-web-report + ''' + } + post { + always { + junit 'report.xml' + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'arachni-web-report', + reportFiles: 'index.html', + reportName: 'Arachni Web Crawl' + ] + } + } + } + } + } + } +} +``` + + + 8. With this config in place, commit your code (from your terminal). Wait for a few minutes until a new build in Jenkins is triggered: ```bash @@ -236,7 +1489,12 @@ NOTE - your build may have failed, or marked as unstable because of the a securi 2. Open the `Jenkinsfile` in the root of the project; move to the `stage("node-build"){ ... }` section. In the `post` section add a block for producing a `HTML` report as part of our builds. This is all that is needed for Jenkins to report the coverage stats. -πŸ“ *Jenkinsfile* +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy // Post can be used both on individual stages and for the entire build. post { @@ -255,7 +1513,300 @@ NOTE - your build may have failed, or marked as unstable because of the a securi } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'reports/coverage', + reportFiles: 'index.html', + reportName: 'Code Coverage' + ] + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + post { + always { + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '', + reportFiles: 'index.html', + reportName: 'Zap Branniscan' + ] + } + } + } + stage('Arachni') { + agent { + node { + label "jenkins-agent-arachni" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + /arachni/bin/arachni https://${E2E_TEST_ROUTE} --report-save-path=arachni-report.afr + /arachni/bin/arachni_reporter arachni-report.afr --reporter=xunit:outfile=report.xml --reporter=html:outfile=web-report.zip + unzip web-report.zip -d arachni-web-report + ''' + } + post { + always { + junit 'report.xml' + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'arachni-web-report', + reportFiles: 'index.html', + reportName: 'Arachni Web Crawl' + ] + } + } + } + } + } + } +} +``` + + + 3. To get the linting working; we will add a new step to our `stage("node-build"){ }` section to lint the JavaScript code. Continuing in the `Jenkinsfile`, After the `npm install`; add a command to run the linting. + +πŸ“ todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy echo '### Install deps ###' sh 'npm install' @@ -263,6 +1814,295 @@ echo '### Running linting ###' sh 'npm run lint' ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running linting ###' + sh 'npm run lint' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'reports/coverage', + reportFiles: 'index.html', + reportName: 'Code Coverage' + ] + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + post { + always { + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '', + reportFiles: 'index.html', + reportName: 'Zap Branniscan' + ] + } + } + } + stage('Arachni') { + agent { + node { + label "jenkins-agent-arachni" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + /arachni/bin/arachni https://${E2E_TEST_ROUTE} --report-save-path=arachni-report.afr + /arachni/bin/arachni_reporter arachni-report.afr --reporter=xunit:outfile=report.xml --reporter=html:outfile=web-report.zip + unzip web-report.zip -d arachni-web-report + ''' + } + post { + always { + junit 'report.xml' + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'arachni-web-report', + reportFiles: 'index.html', + reportName: 'Arachni Web Crawl' + ] + } + } + } + } + } + } +} +``` + + + 4. Save the `Jenkinsfile` and commit it to trigger a build with some more enhancements. ```bash git add . @@ -279,13 +2119,102 @@ git push 6. Fix the error identified by the linter by commenting out the offending line. -πŸ“ src/components/TodoItem.vue -```html +πŸ“ src/components/TodoItem.vue + + + +#### ** Important Part ** + +```javascript Vue.component("checkbox", Checkbox); Vue.component("radio", Radio); // let biscuits; ``` +#### ** Entire File ** + +```html + + + + + + + +``` + + + 7. Save the `TodoItem.vue` and Commit and push your changes to trigger a new build. ```bash From a1bfb99fe97277844094f953b2f6f563b645f0c9 Mon Sep 17 00:00:00 2001 From: Jacob See Date: Mon, 1 Feb 2021 19:06:49 -0800 Subject: [PATCH 6/7] Include Spencer's changes to exercise 3, and add the tabbed interface on top of them Co-authored-by: Spencer Stolworthy --- .../README.md | 1354 +++++------------ 1 file changed, 409 insertions(+), 945 deletions(-) diff --git a/exercises/3-revenge-of-the-automated-testing/README.md b/exercises/3-revenge-of-the-automated-testing/README.md index 99ae528f..6f781d13 100644 --- a/exercises/3-revenge-of-the-automated-testing/README.md +++ b/exercises/3-revenge-of-the-automated-testing/README.md @@ -8,7 +8,7 @@ ## Introduction to TDD. -**Test Driven Development (TDD)** is a software development process that relies on the repetition of a very short development cycle. Requirements are turned into test cases, where the software is developed to pass the tests. In other words, it creates a safety net that serves to keep the developer's problems/bugs at bay while enabling the developer to refactor efficiently. This is opposed to software development that allows software to be added that is not proven to meet requirements. +**Test Driven Development (TDD)** is a software development process that relies on the repetition of a very short development cycle. Requirements are turned into test cases, and the software is developed to pass the tests. In other words, it creates a safety net that serves to keep the developer's problems/bugs at bay while enabling the developer to refactor efficiently. This is opposed to software development that allows software to be added that is not proven to meet requirements. The TDD cycle can be illustrated with the following diagram: @@ -17,30 +17,31 @@ The TDD cycle can be illustrated with the following diagram: ### The TDD Cycle 1. `Write a test` - -In TDD a new feature begins by writing a test. Write a test that clearly defines a function or one that provides an improvement to an existing function. It's important the developer clearly understands the feature's specification and requirements, or the feature could be wrong from the get-go. + In TDD a new feature begins by writing a test. Write a test that clearly defines a function or one that provides an improvement to an existing function. It's important the developer clearly understands the feature's specification and requirements, or the feature could be wrong from the get-go. -2. `Test Fails` - -When a test is first implemented it is expected to fail. This failure validates the test is working correctly as the feature is yet to be implemented. +2. `The test fails` - + When a test is first implemented it is expected to fail. This failure validates the test is working correctly as the feature is yet to be implemented. -3. `Write code to make test pass` - -This step involves implementing the feature to pass the failed test. Code written at this stage may be inelegant and still pass the test, however this is acceptable as TDD is a recursive cycle which includes code refactoring. +3. `Write code to make the test pass` - + This step involves implementing the feature to pass the failed test. Code written at this stage may be inelegant and still pass the test, however this is acceptable as TDD is a recursive cycle which includes code refactoring. -4. `Code Passes tests` - -If all tests pass, the developer can be confident that the new code meets the test requirements. +4. `The test passes` - + If all tests pass, the developer can be confident that the new code meets the test requirements. 5. `Refactor` - -The refactoring step will allow the developer to clean up their code without changing its behaviour. Not changing the behaviour should ensure the tests still pass. The process of refactoring can include; removal of duplication, renaming of object, class, module, variable and method names to clearly represent their current purpose and use, decoupling of functionality and increasing code cohesion. + The refactoring step will allow the developer to clean up their code without changing its behaviour. Not changing the behaviour should ensure the tests still pass. The process of refactoring can include; removal of duplication, renaming of object, class, module, variable and method names to clearly represent their current purpose and use, decoupling of functionality and increasing code cohesion. 6. `Repeat` - -Starting with another new test, the cycle is then repeated to push forward the functionality. The size of the steps should always be small, with as few as 1 to 10 edits between each test run. If new code does not rapidly satisfy a new test, or other tests fail unexpectedly, the programmer should undo or revert in preference to excessive debugging. + Starting with another new test, the cycle is then repeated to push forward the functionality. The size of the steps should always be small, with as few as 1 to 10 edits between each test run. If new code does not rapidly satisfy a new test, or other tests fail unexpectedly, the programmer should undo or revert rather than excessively debuggin. ### Testing Bananalogy -Explanation of Mocha and JS test syntax through Bananalogy! Imagine for a moment; we're not building software but creating a bowl of fruit. To create a `Bunch of Bananas` component for our fruit bowl we could start with our tests as shown below. + +Explanation of Mocha and JS test syntax through a Bananalogy! Imagine for a moment that we're not building software but creating a bowl of fruit. To create a `Bunch of Bananas` component for our fruit bowl we could start with our tests as shown below. ![bdd-bananas](../images/exercise3/bdd-bananas.png) - * `describe` is used to group tests together. The string `"a bunch of ripe bananas"` is for human reading and allows you to identify tests. - * `it` is a statement that contains a test. It should contain an assertion such as `expect` or `should`. It follows the syntax of `describe` where the string passed in identifies the statement. +- `describe` is used to group tests together. The string `"a bunch of ripe bananas"` is for human reading and allows you to identify tests. +- `it` is a statement that contains a test. It should contain an assertion such as `expect` or `should`. It follows the syntax of `describe` where the string passed in identifies the statement. --- @@ -48,21 +49,22 @@ Explanation of Mocha and JS test syntax through Bananalogy! Imagine for a moment As a learner you will be able to -* Understand the why behind TDD -* Implement a feature using TDD for front end and backend -* Write end to end tests for the feature and run them in CI +- Understand the why behind TDD +- Implement a feature using TDD for front end and backend +- Write end-to-end tests for a feature and run them in the CI pipeline ## Tools and Frameworks 1. [Jest](https://facebook.github.io/jest/) - Zero configuration testing platform -Jest is used by Facebook to test all JavaScript code including React applications. One of Jest's philosophies is to provide an integrated "zero-configuration" experience. We observed that when engineers are provided with ready-to-use tools, they end up writing more tests, which in turn results in more stable and healthy code bases. + Jest is used by Facebook to test all JavaScript code including React applications. One of Jest's philosophies is to provide an integrated "zero-configuration" experience. We observed that when engineers are provided with ready-to-use tools, they end up writing more tests, which in turn results in more stable and healthy code bases. 1. [Vue Test Utils](https://vue-test-utils.vuejs.org/en/) - Vue Test Utils is the official unit testing utility library for Vue.js. 1. [Nightwatch.js](http://nightwatchjs.org/) - Nightwatch.js is an easy to use Node.js based End-to-End (E2E) testing solution for browser based apps and websites. It uses the powerful W3C WebDriver API to perform commands and assertions on DOM elements. -1. [Mocha](https://mochajs.org/) - Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. Hosted on GitHub. +1. [Mocha](https://mochajs.org/) - Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. 1. [Sinon](http://sinonjs.org/) - Standalone test spies, stubs and mocks for JavaScript. -Works with any unit testing framework. + Works with any unit testing framework. ## Big Picture + > From the previous exercise; we created a simple pipeline. We will now flesh it out with some testing to add gates to our pathway to production. ![big-picture](../images/big-picture/big-picture-3.jpg) @@ -88,12 +90,15 @@ _On page load:_ ## Step by Step Instructions ### Part 1 - Tests in our Pipeline -> _In this part we will get familiar with the layout of our tests. We will also improve the pipeline created already by adding some unit tests for the front end & backend along with some end to end tests (e2e) to validate the full solution_ + +> _In this part we will get familiar with the layout of our tests. We will also improve the pipeline created already by adding some unit tests for the frontend and backend along with some end-to-end tests (e2e) to validate the full solution_ #### 1a - Unit tests + > In this exercise we will execute our test for the front end locally. Once verified we will add them to Jenkins. -1. Before linking our automated testing to the pipeline we'll first ensure the tests run locally. Change to the `todolist` directory and run `test` on the `develop` branch. +1. Before linking our automated testing to the pipeline, we'll first ensure the tests run locally. Change to the `todolist` directory and run `test` on the `develop` branch. + ```bash cd /projects/todolist ``` @@ -105,6 +110,7 @@ git checkout develop ```bash npm run test:client ``` +

NOTE - test:client is an alias used that runs vue-cli-service test from the scripts object in package.json

@@ -115,18 +121,22 @@ npm run test:client ![test-run-locally](../images/exercise3/test-run-locally.png) -4. Let's now try and get our Server tests running locally. Ensure your Database is running by opening a new terminal session and run the following command. This will start our mongodb locally. +4. Let's now try and get our Server tests running locally. Ensure your Database is running by opening a new terminal session and run the following command. This will start our mongodb locally. + ```bash npm run mongo:start-ide ``` +

NOTE - you can skip this step if you have your DB running from previous exercise

-5. Run your server side test with the following command. This will fire some requests against the API, validating that CRUD is working. +5. Run your server side test with the following command. This will fire some requests against the API, validating that CRUD is working. + ```bash npm run test:server ``` + ![test-server-run-locally](../images/exercise3/test-server-run-locally.png)

@@ -149,7 +159,7 @@ steps { sh 'npm install' echo '### Running tests ###' - sh 'npm run test:all:ci + sh 'npm run test:all:ci' ``` #### ** Entire File ** @@ -338,12 +348,11 @@ pipeline { } } } - ``` -7. Running the tests is important, but so is reporting the results. In the `post{}` `always{}` section of the `Jenkinsfile` add the location for Jenkins for find the test reports +7. Running the tests is important, but so is reporting the results. In the `post{}` `always{}` section of the `Jenkinsfile`, add the location for Jenkins to find the test reports πŸ“ todolist/Jenkinsfile @@ -359,6 +368,7 @@ post { junit 'test-report.xml' junit 'reports/server/mocha/test-results.xml' } +} ``` #### ** Entire File ** @@ -462,8 +472,6 @@ pipeline { // ADD TESTS REPORTS HERE junit 'test-report.xml' junit 'reports/server/mocha/test-results.xml' - // publish html - } success { echo "Git tagging" @@ -548,12 +556,12 @@ pipeline { } } } - ``` -8. With this in place, commit the changes which should trigger a build. +8. With this in place, commit the changes, which should trigger a build. + ```bash git add Jenkinsfile git commit -m "Adding unit tests to the pipeline" @@ -562,18 +570,20 @@ git push 9. Navigate to your instance of Jenkins at `https://jenkins--ci-cd.` and you should see the tests are now running in your pipeline. You should see a test trend graph only after two runs of the build - #### 1b - End to End Tests (e2e) + > _Unit tests are a great way to get immediate feedback as part of testing an application. End to end tests that drive user behaviour are another amazing way to ensure an application is behaving as expected._ In this part of the exercise, we will add a new stage to our pipeline called `todolist-e2e` that will run after the deploy has been completed. End to end tests will use `Nightwatch.js` to orchestrate a Selenium WebDriver instance that controls the web browser; in this case Google Chrome! 1. Ensure the `todolist` app is running in a separate terminal shell. + ``` npm run serve:all ``` 2. Let's start by checking that our tests execute in the cloud ide. Our end to end tests are stored in `tests/e2e/specs/`. The VueJS cli uses `Nightwatch.js` and comes pre-configured to run tests against Google Chrome. The tests run headlessly in our CodeReady workspace. To get them executing, open a new Terminal and fire up the Selenium service and leave it running. + ```bash cd /projects/todolist npm run selenium @@ -584,6 +594,7 @@ npm run selenium

3. On a new terminal move to the `todolist` folder. Run the tests locally by executing the following command. This should start the dev server and run the test. + ```bash cd /projects/todolist npm run e2e:ide @@ -601,7 +612,7 @@ npm run e2e:ide ```groovy stage("e2e test") { - + } ``` @@ -706,8 +717,6 @@ pipeline { // ADD TESTS REPORTS HERE junit 'test-report.xml' junit 'reports/server/mocha/test-results.xml' - // publish html - } success { echo "Git tagging" @@ -795,7 +804,6 @@ pipeline { } } } - ``` @@ -938,8 +946,6 @@ pipeline { // ADD TESTS REPORTS HERE junit 'test-report.xml' junit 'reports/server/mocha/test-results.xml' - // publish html - } success { echo "Git tagging" @@ -1048,12 +1054,12 @@ pipeline { } } } - ``` -6. With this in place, commit the changes to trigger a build and enhance our pipeline +6. With this in place, commit the changes to trigger a build and enhance our pipeline + ```bash git add Jenkinsfile git commit -m "Adding e2e tests to the pipeline" @@ -1061,9 +1067,10 @@ git push ``` 7. Jenkins should now show the additional stage in the pipeline view for the branch -![e2e-pipeline](../images/exercise3/e2e-pipeline.png) + ![e2e-pipeline](../images/exercise3/e2e-pipeline.png) 8. After confirming the pipeline is successful on the `develop` branch, let's bring these changes back into the main branch. + ```bash git checkout master ``` @@ -1080,23 +1087,28 @@ git commit -m "Updated Jenkinsfile taken from develop branch" git push ``` - ### Part 2 - TodoList new feature -> _In this exercise we will introduce a new feature to create an important flag on the todos. In order to be able to build and test our feature we will use TDD_ -*As a doer I want to mark todos as important so that I can keep track of and complete high priority todos first* +> _In this exercise we will introduce a new feature to create an important flag on the todos. We will use Test Driven Development to build and test our feature_ + +Let's look at a user story for the new feature we want to add: + +_As a todolist user, I want to mark todos as important so that I can keep track of and complete high priority todos first_ _Acceptance Criteria_ + - [ ] should be doable with a single click - [ ] should add a red flag against the todo when marked important - [ ] should remove the red colour flag on the flag when important removed - [ ] should not affect existing todos _On page load:_ + - [ ] should display existing todos that are not marked important - [ ] should display existing todos that are marked important with an red flag #### 2a - Create todolist api tests + > Using [Mocha](https://mochajs.org/) as our test runner; we will now write some tests for backend functionality to persist our important-flag. The changes required to the backend are minimal but we will use TDD to create our test first, then implement the functionality. 1. Create a new branch in your `todolist` app for our feature and push it to the remote @@ -1104,55 +1116,66 @@ _On page load:_ ```bash cd todolist ``` + ```bash git checkout -b feature/important-flag ``` + ```bash git push -u origin feature/important-flag ``` 2. Navigate to the `server/api/todo/todo.spec.js` file. This contains all of the existing todo list api tests. These are broken down into simple `describe("api definition", function(){})` blocks which is BDD speak for how the component being tested should behave. Inside of each `it("should do something ", function(){})` statements we use some snappy language to illustrate the expected behaviour of the test. For example a `GET` request of the api is described and tested for the return to be of type Array as follows. -πŸ‘€ todolist/server/api/todo/todo.spec.js +πŸ‘€ todolist/server/api/todo/todo.spec.jsπŸ“ todolist/server/api/todo/todo.spec.js @@ -1162,24 +1185,24 @@ npm run test:server ```javascript // Exercise 3 test case! -it("should mark todo as important and persist it", function(done) { - request(app) - .put("/api/todos/" + todoId) - .send({ - title: "LOVE endpoint/server side testing!", - completed: true, - important: true - }) - .expect(200) - .expect("Content-Type", /json/) - .end(function(err, res) { - if (err) return done(err); - res.body.should.have.property("_id"); - res.body.title.should.equal("LOVE endpoint/server side testing!"); - // YOUR TEST GO HERE - res.body.important.should.equal(true); - done(); - }); +it("should mark todo as important and persist it", function (done) { + request(app) + .put("/api/todos/" + todoId) + .send({ + title: "LOVE endpoint/server side testing!", + completed: true, + important: true, + }) + .expect(200) + .expect("Content-Type", /json/) + .end(function (err, res) { + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("LOVE endpoint/server side testing!"); + // YOUR TEST GO HERE + res.body.important.should.equal(true); + done(); + }); }); ``` @@ -1381,17 +1404,17 @@ describe("PUT /api/todos/:id", function() { // Exercise 3 test case! - it("should ....", function(done) { + it("should mark todo as important and persist it", function (done) { request(app) .put("/api/todos/" + todoId) .send({ title: "LOVE endpoint/server side testing!", completed: true, - important: true + important: true, }) .expect(200) .expect("Content-Type", /json/) - .end(function(err, res) { + .end(function (err, res) { if (err) return done(err); res.body.should.have.property("_id"); res.body.title.should.equal("LOVE endpoint/server side testing!"); @@ -1402,19 +1425,20 @@ describe("PUT /api/todos/:id", function() { }); }); - ``` 6. Run your test. It should fail. + ```bash npm run test:server ``` ![fail-mocha](../images/exercise3/fail-mocha.png) -7. With our test now failing; let's implement the feature. This is quite a simple change - we first need to update the `server/api/todo/todo.model.js`. Add an additional property on the schema called `important` and make its type Boolean. +7. With our test now failing, let's implement the feature. This is quite a simple changeβ€”we first need to update the `server/api/todo/todo.model.js`. + Add an additional property on the schema called `important` and make its type `Boolean`. πŸ“ todolist/server/api/todo/todo.model.js @@ -1425,8 +1449,8 @@ npm run test:server ```javascript const TodoSchema = new Schema({ title: String, - completed: Boolean, - important: Boolean + completed: Boolean, + important: Boolean, }); ``` @@ -1441,7 +1465,7 @@ const mongoose = require('mongoose'), const TodoSchema = new Schema({ title: String, completed: Boolean, - important: Boolean + important: Boolean, }); module.exports = mongoose.model('Todo', TodoSchema); @@ -1451,66 +1475,32 @@ module.exports = mongoose.model('Todo', TodoSchema); 8. Next we need to update the `server/config/seed.js` file so that the pre-generated todos have an important property. Add `important: false` below `completed: *` for each object. Don't forget to add a comma at the end of the `completed: *` line. -πŸ“ todolist/server/config/seed.js - - - -#### ** Important Part ** - -```javascript -completed: false, -important: false -``` - -#### ** Entire File ** - -```javascript -/** - * Populate DB with sample data on server start - * to disable, edit config/environment/index.js, and set `seedDB: false` - */ - -'use strict'; - -const Todo = require('../api/todo/todo.model'); - -Todo.find({}).remove(function() { - Todo.create({ - title : 'Learn some stuff about MongoDB', - completed: false, - important: false - }, { - title : 'Play with NodeJS', - completed: true, - important: false - }); -}); - -``` - - - ![api-add-seed-important](../images/exercise3/api-add-seed-important.png) 9. With your changes to the Database schema updated; re-run your tests. The tests should pass. + ```bash npm run test:server ``` -10. Commit your code to the `feature/important-flag` branch and then merge onto the `develop` branch as follows +10. Commit your code to the `feature/important-flag` branch and then merge onto the `develop` branch as follows ```bash git add . ``` + ```bash git commit -m "ADD backend schema updates" ``` + ```bash git push ``` + ```bash git checkout develop ``` + ```bash git merge feature/important-flag ``` @@ -1522,54 +1512,73 @@ git push ``` #### 2b - Create todolist front-end tests + > Using [Jest](https://facebook.github.io/jest/) as our test runner and the `vue-test-utils` library for managing our vue components; we will now write some tests for front end functionality to persist our important-flag. The changes required to the front end are quite large but we will use TDD to create our test first, then implement the functionality. -Our TodoList App uses `vuex` to manage the state of the app's todos and `axios` HTTP library to connect to the backend. `Vuex` is an opinionated framework for managing application state and has some key design features you will need to know to continue with the exercise. +Our TodoList App uses `vuex` to manage the state of the app's todos and `axios` HTTP library to connect to the backend. +`Vuex` is an opinionated framework for managing application state and has some key design features you will need to know to continue with the exercise. -In `vuex` the application state is managed by a `store`. The `store` houses all the todos we have retrieved from the backend as well as the `getter` methods for our array of `todos`. In order to make changes to the store, we could call the store directly and update each todo item but as earlier said; vuex is an opinionated module with its own way of updating the store. It is bad practice to call the store directly. +In `vuex` the application state is managed by a `store`. The `store` houses all the todos we have retrieved from the backend +as well as the `getter` methods for our array of `todos`. In order to make changes to the store, we could call the store directly +and update each todo item but as earlier said; vuex is an opinionated module with its own way of updating the store. +It is bad practice to call the store directly. -There are two parts of the lifecycle to updating the store, the `actions` & `mutations`. When the user clicks a todo to mark it as complete; the `actions` are called. An action could involve a call to the backend or some pre-processing of the data. Once this is done, the change is committed to the store by calling the `mutation` function. A store should only ever be manipulated through a mutation function. Calling the mutation will then update the todo object in the app's local store for rendering in the view. +There are two parts of the lifecycle to updating the store, the `actions` & `mutations`. When the user clicks a todo to mark it as complete, +the `actions` are called. An action could involve a call to the backend or some pre-processing of the data. Once this is done, +the change is committed to the store by calling the `mutation` function. A store should only ever be manipulated through a mutation function. +Calling the mutation will then update the todo object in the app's local store for rendering in the view. -For example; when marking a todo as done in the UI, the following flow occurs - * The `TodoItem.vue` calls the `markTodoDone()` function which dispatches an event to the store. - * This calls the `updateTodo()` function in the `actions.js` file - * The action will update the backend db (calling our `todolist api`) with our updated todo object. - * The action will commit the change to the store by calling the mutation method `MARK_TODO_COMPLETED` - * The `MARK_TODO_COMPLETED` will directly access the store object and update it with the new state value - * The `ListOfTodos.vue` component is watching the store for changes and when something gets updated it re-renders the `TodoItem.vue`. +For example, when marking a todo as done in the UI, the following flow occurs: + +- The `TodoItem.vue` calls the `markTodoDone()` function which dispatches an event to the store. +- This calls the `updateTodo()` function in the `actions.js` file +- The action will update the backend db (calling our `todolist api`) with our updated todo object. +- The action will commit the change to the store by calling the mutation method `MARK_TODO_COMPLETED` +- The `MARK_TODO_COMPLETED` will directly access the store object and update it with the new state value +- The `ListOfTodos.vue` component is watching the store for changes and when something gets updated it re-renders the `TodoItem.vue`. The implementation of our `important` flag will follow this same flow. 1. Let's implement our feature by first creating a branch. Our new feature, important flag will behave in the same way as the `MARK_TODO_COMPLETED`. Create a new branch in your `todolist` app for our feature and push it to the remote -```bash -cd todolist -``` -```bash -git checkout feature/important-flag -``` -```bash -git push -u origin feature/important-flag -``` + ```bash + cd todolist + ``` -2. Let's get our tests running by executing a `--watch` on our tests. This will keep re-running our tests everytime there is a file change. It is handy to have this running in a new terminal session. -```bash -npm run test:client -- --watch -``` + ```bash + git checkout feature/important-flag + ``` -3. All the tests should be passing when we begin. If `No tests found related to files changed since last commit` is on show; hit `a` on the terminal to re-run `all` tests. + ```bash + git push -u origin feature/important-flag + ``` -![rerun-all](../images/exercise3/rerun-all.png) +1. Let's get our tests running by executing a `--watch` on our tests. This will keep re-running our tests everytime there is a file change. It is handy to have this running in a new terminal session. -4. We will add new tests in three places to validate our function behaves as expected against the acceptance criteria from the Feature Story supplied to us. We will need to write tests for our `TodoItem.vue` to verify having a red flag and that it is clickable. Our app is going to need to persist the changes in the backend so we'll want to make changes to our `actions.js` and `mutations.js` to keep the API and local copy of the store in sync. Let's start with our `TodoItem.vue` component. Open the `tests/unit/vue-components/TodoItem.spec.js` file. This has been templated with some example test to correspond with our A/Cs for speed of doing the exercise. Find the describe block for our important flag tests. It is set up already with a `beforeEach()` hook for test setup. + ```bash + npm run test:client -- --watch + ``` -![important-flag-before](../images/exercise3/important-flag-before.png) +1. All the tests should be passing when we begin. If `No tests found related to files changed since last commit` is showing, hit `a` on the terminal to re-run `all` tests. -5. Each of our test cases has its skeleton in place already for example the `TodoItem.vue` component takes a property of `todos` when rendering. This setup is already done for each of our tests so all we have to do is fill in our assertions. + ![rerun-all](../images/exercise3/rerun-all.png) -![todoitem-skeleton-tests](../images/exercise3/todoitem-skeleton-tests.png) +1. We will add new tests in three places to validate our function behaves as defined in the Feature Story. + We will need to write tests for `TodoItem.vue` to verify that it has a clickable red flag. + Our app is going to need to persist the changes in the backend, so we will want to modify `actions.js` and `mutations.js` to keep the API and local copy of the store in sync. + Let's start with our `TodoItem.vue` component. Open the `tests/unit/vue-components/TodoItem.spec.js` file. + Notice that this file already contains a few tests that validate some of our AC's (acceptance criteria). + Find the `describe` block for our important flag tests. + The block contains a `beforeEach()` hook, which is a special function that is called before each test in the `describe` block is run. -6. Let's implement the first test `it("should render a button with important flag"`. This test will assert if the button is present on the page and it contains the `.important-flag` CSS class. To implement this; add the `expect` statement as follows below the `// TODO - test goes here!` comment. + ![important-flag-before](../images/exercise3/important-flag-before.png) + +1. Each of our test cases has its skeleton in place already for example the `TodoItem.vue` component takes a property of `todos` when rendering. This setup is already done for each of our tests so all we have to do is fill in our assertions. + + ![todoitem-skeleton-tests](../images/exercise3/todoitem-skeleton-tests.png) + +1. Let's implement the first test `it("should render a button with important flag", ...)`. This test will assert that the button is present on the page and it contains the `.important-flag` CSS class. + To implement this, add the `expect` statement below the `// TODO - test goes here!` comment. πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js @@ -1578,19 +1587,19 @@ npm run test:client -- --watch #### ** Important Part ** ```javascript - it("should render a button with important flag", () => { - const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } - }); - // TODO - test goes here! - expect(wrapper.find(".important-flag").exists()).toBe(true); +it("should render a button with important flag", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo }, }); + // TODO - test goes here! + expect(wrapper.find(".important-flag").exists()).toBe(true); +}); ``` #### ** Entire File ** ```javascript -/* eslint-disable */ + /* eslint-disable */ import { shallow, mount, createLocalVue } from "@vue/test-utils"; import Vuex from "vuex"; import TodoItem from "@/components/TodoItem.vue"; @@ -1663,7 +1672,7 @@ describe("Important Flag button ", () => { }); // TODO - test goes here! }); - it("call markImportant when clicked", () => { + it("call makImportant when clicked", () => { const wrapper = mount(TodoItem, { methods, propsData: { todoItem: importantTodo } @@ -1675,11 +1684,13 @@ describe("Important Flag button ", () => { -7. Save the file. Observe that the test case has started failing because we have not yet implemented the feature! +1. Save the file. Observe that the test case has started failing because we have not yet implemented the feature! -![todoitem-fail-test](../images/exercise3/todoitem-fail-test.png) + ![todoitem-fail-test](../images/exercise3/todoitem-fail-test.png) -8. With a basic assertion in place, let's continue on to the next few tests. We want the important flag to be red when an item in the todolist is marked accordingly. Conversely we want it to be not red when false. Let's create a check for `.red-flag` CSS property to be present when important is true and not when false. Complete the `expect` statements in your test file as shown below for both tests. +1. With a basic assertion in place, let's continue on to the next few tests. We want the important flag to be red when an item in the todolist is marked important. + Conversely, we want it to not be red when important is false. + Complete the `expect` statements in your test file as shown below for both tests. πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js @@ -1690,7 +1701,7 @@ describe("Important Flag button ", () => { ```javascript it("should set the colour to red when true", () => { const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(true); @@ -1698,7 +1709,7 @@ describe("Important Flag button ", () => { it("should set the colour to not red when false", () => { importantTodo.important = false; const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(false); @@ -1770,7 +1781,7 @@ describe("Important Flag button ", () => { }); it("should set the colour to red when true", () => { const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(true); @@ -1778,12 +1789,12 @@ describe("Important Flag button ", () => { it("should set the colour to not red when false", () => { importantTodo.important = false; const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(false); }); - it("call markImportant when clicked", () => { + it("call makImportant when clicked", () => { const wrapper = mount(TodoItem, { methods, propsData: { todoItem: importantTodo } @@ -1791,12 +1802,12 @@ describe("Important Flag button ", () => { // TODO - test goes here! }); }); - ``` -9. Finally, we want to make the flag clickable and for it to call a function to update the state. The final test in the `TodoItem.spec.js` we want to create should simulate this behaviour. Implement the `it("call markImportant when clicked", () ` test by first simulating the click of our important-flag and asserting the function `markImportant()` to write is executed. +1. Finally, we want to make the flag clickable and for it to call a function to update the state. The final test in the `TodoItem.spec.js` should simulate this behaviour. + Implement the `it("call markImportant when clicked", () ` test by first simulating the click of our important-flag and asserting the function `markImportant()` is executed. πŸ“ todolist/tests/unit/vue-components/TodoItem.spec.js @@ -1806,16 +1817,16 @@ describe("Important Flag button ", () => { #### ** Important Part ** ```javascript - it("call markImportant when clicked", () => { - const wrapper = mount(TodoItem, { - methods, - propsData: { todoItem: importantTodo } - }); - // TODO - test goes here! - const input = wrapper.find(".important-flag"); - input.trigger("click"); - expect(methods.markImportant).toHaveBeenCalled(); - }); +it("call markImportant when clicked", () => { + const wrapper = mount(TodoItem, { + methods, + propsData: { todoItem: importantTodo }, + }); + // TODO - test goes here! + const input = wrapper.find(".important-flag"); + input.trigger("click"); + expect(methods.markImportant).toHaveBeenCalled(); +}); ``` #### ** Entire File ** @@ -1883,7 +1894,7 @@ describe("Important Flag button ", () => { }); it("should set the colour to red when true", () => { const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(true); @@ -1891,7 +1902,7 @@ describe("Important Flag button ", () => { it("should set the colour to not red when false", () => { importantTodo.important = false; const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(false); @@ -1899,7 +1910,7 @@ describe("Important Flag button ", () => { it("call markImportant when clicked", () => { const wrapper = mount(TodoItem, { methods, - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! const input = wrapper.find(".important-flag"); @@ -1907,18 +1918,17 @@ describe("Important Flag button ", () => { expect(methods.markImportant).toHaveBeenCalled(); }); }); - ``` -10. With our tests written for the feature's UI component, let's implement our code to pass the tests. Explore the `src/components/TodoItem.vue`. Each vue file is broken down into 3 sections +1. With our tests written for the feature's UI component, let's implement our code to pass the tests. Open `src/components/TodoItem.vue`. Each vue file is broken down into 3 sections - * The `` contains the HTML of our component. This could include references to other Components also - * The `` contains the JavaScript of our component and is essentially the logic for our component. It defines things like `properties`, `methods` and other `components` - * The `` contains the encapsulated CSS of our component + - The `` contains the HTML of our component. This could include references to other Components also + - The `` contains the JavaScript of our component and is essentially the logic for our component. It defines things like `properties`, `methods` and other `components` + - The `` contains the encapsulated CSS of our component -11. Underneath the `` tag, let's add a new md-button. Add an `.important-flag` class on the `md-button` and put the svg of the flag provided inside it. +1. Underneath the `` tag, let's add a new md-button. Add the `.important-flag` class on the `md-button` and put the provided svg of the flag inside it. πŸ“ todolist/src/components/TodoItem.vue @@ -1926,7 +1936,7 @@ describe("Important Flag button ", () => { #### ** Important Part ** -```html + ```html @@ -1945,12 +1955,12 @@ describe("Important Flag button ", () => { > - {{ todoItem.title }} - - + {{ todoItem.title }} + + - + @@ -2010,18 +2020,19 @@ export default { fill: #cc0000; } - ``` -12. We should now see the first of our failing tests has started to pass. Running the app locally (using `npm run serve`) should show the flag appear in the UI. It is clickable but won't fire any events and the colour is not red as per our requirement. +1. We should now see the first of our failing tests has started to pass. + Running the app locally (using `npm run serve`) should show the flag in the UI. + It is clickable, but it won't fire any events. Additionally, the colour is not red as required in the acceptance criteria. -

-NOTE - If you don't see the important flag in the CRW UI preview window, an old UI version might be cached. Try to use the external link provided by CRW. -

+

+ NOTE - If you don't see the important flag in the CRW UI preview window, an old UI version might be cached. Try to use the external link provided by CRW. +

-Let's continue to implement the colour change for the flag. On our `` tag, add some logic to bind the css to the property of a `todo.important` by adding ` :class="{'red-flag': todoItem.important}" `. This logic will apply the CSS class when `todo.important` is true. + Let's continue to implement the colour change for the flag. On our `` tag, add some logic to bind the css to the property of a `todo.important` by adding `:class="{'red-flag': todoItem.important}" `. This logic will apply the CSS class when `todo.important` is true. πŸ“ todolist/src/components/TodoItem.vue @@ -2030,9 +2041,18 @@ Let's continue to implement the colour change for the flag. On our `` tag, #### ** Important Part ** ```html - - - + + + + + + ``` #### ** Entire File ** @@ -2049,9 +2069,18 @@ Let's continue to implement the colour change for the flag. On our `` tag, {{ todoItem.title }} - - - + + + + + + @@ -2111,12 +2140,14 @@ export default { fill: #cc0000; } - ``` -13. More tests should now be passing. Let's wire the click of the flag to an event in Javascript. In the methods section of the `` tags in the Vue file, implement the `markImportant()`. We want to wire this to the action to updateTodo, just like we have in the `markCompleted()` call above it. We also need to pass an additional property to this method called `important` +1. More tests should now be passing. Let's wire the click of the flag to an event in Javascript. + In the methods section of the `` tags in the Vue file, implement `markImportant()`. + We want to wire this to the action `updateTodo`, just like we have in the `markCompleted()` call. + We also need to pass an additional property to this method called `important` πŸ“ todolist/src/components/TodoItem.vue @@ -2146,9 +2177,18 @@ export default { {{ todoItem.title }} - - - + + + + + + @@ -2210,12 +2250,12 @@ export default { fill: #cc0000; } - ``` -14. Let's connect the click button in the DOM to the Javascript function we've just created. In the template, add a click handler to the md-button to call the function `markImportant()` by adding ` @click="markImportant()"` to the `` tag +1. Let's connect the click button in the DOM to the Javascript function we've just created. + In the template, add a click handler to the md-button to call the function `markImportant()` by adding ` @click="markImportant()"` to the `` tag. πŸ“ todolist/src/components/TodoItem.vue @@ -2224,10 +2264,19 @@ export default { #### ** Important Part ** ```html - - - - + + + + + + + ``` #### ** Entire File ** @@ -2244,9 +2293,18 @@ export default { {{ todoItem.title }} - - - + + + + + + @@ -2308,596 +2366,66 @@ export default { fill: #cc0000; } - ``` -15. Finally - we need to make it so that when a new todo item is created it will have an important property. Head to `src/store/actions.js` and add `important: false` below `completed: false` in the `addTodo(){}` action. - -![fe-add-actions-important](../images/exercise3/fe-add-actions-important.jpg) - - -16. The previously failing tests should have started to pass now. With this work done, let's commit our code. On the terminal, run -```bash -git add . -``` -```bash -git commit -m "Implementing the todoitem flag" -``` -```bash -git push -``` +1. All of our tests should now be passing. On the watch tab where they are running, hit `a` to re-run all tests and update any snapshots with `u` if needed. -17. Start our local development server (if not already running) to manually verify the changes +1. With all of our tests now passing, let's commit our code. On the terminal, run -```bash -npm run serve:all -``` + ```bash + git add . + ``` -18. Open our local todolist app (http://localhost:8080/#/todo). If we try to use our important flag, we should see it's still not behaving as expected; this is because we're not updating the state of the app in response to the click event. + ```bash + git commit -m "Implementing the store and actions" + ``` -19. We need to implement the `actions` and `mutations` for our feature. Let's start with the tests. Open the `tests/unit/javascript/actions.spec.js` and navigate to the bottom of the file. Our action should should commit the `MARK_TODO_IMPORTANT` to the mutations. Scroll to the end of the test file and implement the skeleton test by adding `expect(commit.firstCall.args[0]).toBe("MARK_TODO_IMPORTANT");` as the assertion. + ```bash + git push + ``` -πŸ“ todolist/tests/unit/javascript/actions.spec.js +1. Before running a build in Jenkins, let's add our tests and code to the `develop` branch. + Ask your neighbour to review your code changes and, if they approve, merge them to `develop`! (If you're feeling adventurous, raise a PR through GitLab and have someone else peer review it!) - + ```bash + git checkout develop + ``` -#### ** Important Part ** + ```bash + git merge feature/important-flag + ``` -```javascript - it("should call MARK_TODO_IMPORTANT", done => { - const commit = sinon.spy(); - state.todos = todos; - actions.updateTodo({ commit, state }, { id: 1, important: true }).then(() => { - // TODO - test goes here! - expect(commit.firstCall.args[0]).toBe("MARK_TODO_IMPORTANT"); - done(); - }); - }); -``` + When the editor screen appears in the terminal after running the merge, type `:q` and hit enter to quit the editor. -#### ** Entire File ** - -```javascript -import actions from "@/store/actions"; -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import sinon from "sinon"; -import config from "../../../src/config"; - -const todos = [ - { _id: 1, title: "learn testing", completed: true }, - { _id: 2, title: "learn testing 2", completed: false } -]; -let state; - -describe("loadTodos", () => { - beforeEach(() => { - let mock = new MockAdapter(axios); - mock.onGet(config.todoEndpoint).reply(200, todos); - }); - it("should call commit to the mutation function twice", done => { - const commit = sinon.spy(); - actions.loadTodos({ commit }).then(() => { - // console.log(commit) - expect(commit.calledTwice).toBe(true); - done(); - }); - }); - - it("should first call SET_LOADING", done => { - const commit = sinon.spy(); - actions.loadTodos({ commit }).then(() => { - // console.log(commit.firstCall.args[0]) - expect(commit.firstCall.args[0]).toBe("SET_TODOS"); - done(); - }); - }); - it("should second call SET_TODOS", done => { - const commit = sinon.spy(); - actions.loadTodos({ commit }).then(() => { - // console.log(commit) - expect(commit.secondCall.args[0]).toBe("SET_LOADING"); - done(); - }); - }); -}); - -describe("addTodos", () => { - beforeEach(() => { - state = {}; - let mock = new MockAdapter(axios); - // mock.onPost(/http:\/\/localhost:9000\/api\/todos\/.*/, {}) - mock.onPost(config.todoEndpoint).reply(200, todos); - }); - it("should call commit to the mutation function once", done => { - const commit = sinon.spy(); - state.newTodo = "Learn some mocking"; - actions.addTodo({ commit, state }).then(() => { - // console.log(commit) - expect(commit.calledOnce).toBe(true); - done(); - }); - }); - it("should first call ADD_TODO", done => { - const commit = sinon.spy(); - state.newTodo = "Learn some mocking"; - actions.addTodo({ commit, state }).then(() => { - // console.log(commit.firstCall.args[0]) - expect(commit.firstCall.args[0]).toBe("ADD_TODO"); - done(); - }); - }); -}); - -describe("setNewTodo", () => { - it("should call SET_NEW_TODO", () => { - const commit = sinon.spy(); - actions.setNewTodo({ commit, todo: "learn stuff about mockin" }); - expect(commit.firstCall.args[0]).toBe("SET_NEW_TODO"); - }); -}); - -describe("clearNewTodo", () => { - it("should call CLEAR_NEW_TODO", () => { - const commit = sinon.spy(); - actions.clearNewTodo({ commit }); - expect(commit.firstCall.args[0]).toBe("CLEAR_NEW_TODO"); - }); -}); - -describe("clearTodos", () => { - it("should call CLEAR_ALL_TODOS when all is true", () => { - const commit = sinon.spy(); - state.todos = todos; - actions.clearTodos({ commit, state }, true); - expect(commit.firstCall.args[0]).toBe("CLEAR_ALL_TODOS"); - }); - - it("should call CLEAR_ALL_DONE_TODOS when all is not passed", () => { - const commit = sinon.spy(); - state.todos = todos; - actions.clearTodos({ commit, state }); - expect(commit.firstCall.args[0]).toBe("CLEAR_ALL_DONE_TODOS"); - }); -}); - -describe("updateTodo", () => { - beforeEach(() => { - state = {}; - let mock = new MockAdapter(axios); - mock.onPut(`${config.todoEndpoint}/1`).reply(200, todos); - }); - it("should call commit to the mutation function once", done => { - const commit = sinon.spy(); - state.todos = todos; - actions.updateTodo({ commit, state }, { id: 1 }).then(() => { - expect(commit.calledOnce).toBe(true); - done(); - }); - }); - it("should call MARK_TODO_COMPLETED", done => { - const commit = sinon.spy(); - state.todos = todos; - actions.updateTodo({ commit, state }, { id: 1 }).then(() => { - // console.log(commit.firstCall.args[0]) - expect(commit.firstCall.args[0]).toBe("MARK_TODO_COMPLETED"); - done(); - }); - }); - it("should call MARK_TODO_IMPORTANT", done => { - const commit = sinon.spy(); - state.todos = todos; - actions - .updateTodo({ commit, state }, { id: 1, important: true }) - .then(() => { - // TODO - test goes here! - expect(commit.firstCall.args[0]).toBe("MARK_TODO_IMPORTANT"); - done(); - }); - }); -}); - -``` - - - -20. We should now have more failing tests, let's fix this by adding the call from our action to the mutation method. Open the `src/store/actions.js` file and scroll to the bottom to the `updateTodo()` method. Complete the if block by adding `commit("MARK_TODO_IMPORTANT", i);` as shown below. - -πŸ“ todolist/src/store/actions.js - - - -#### ** Important Part ** - -```javascript -updateTodo({ commit, state }, { id, important }) { - let i = state.todos.findIndex(todo => todo._id === id); - if (important) { - // TODO - add commit important here! - commit("MARK_TODO_IMPORTANT", i); - } else { - commit("MARK_TODO_COMPLETED", i); - } -``` - -#### ** Entire File ** - -```javascript -import axios from "axios"; -import config from "@/config"; - -const dummyData = [ - { - _id: 0, - title: "Learn awesome things about Labs πŸ”¬", - completed: false, - important: false - }, - { - _id: 1, - title: "Learn about my friend Jenkins πŸŽ‰", - completed: true, - important: false - }, - { - _id: 2, - title: "Drink Coffee β˜•πŸ’©", - completed: false, - important: true - } -]; -export default { - loadTodos({ commit }) { - return axios - .get(config.todoEndpoint) - .then(r => r.data) - .then(todos => { - commit("SET_TODOS", todos); - commit("SET_LOADING", false); - }) - .catch(err => { - if (err) { - console.info("INFO - setting dummy data because of ", err); - commit("SET_TODOS", dummyData); - commit("SET_LOADING", false); - } - }); - }, - addTodo({ commit, state }) { - if (!state.newTodo) { - // do not add empty todos - return; - } - // debugger - const todo = { - title: state.newTodo, - completed: false - }; - // console.info("TESTINT BLAH BLAH ", todo); - return axios - .post(config.todoEndpoint, todo) - .then(mongoTodo => { - commit("ADD_TODO", mongoTodo.data); - }) - .catch(err => { - if (err) { - console.info("INFO - Adding dummy todo because of ", err); - let mongoTodo = todo; - mongoTodo._id = "fake-todo-item-" + Math.random(); - commit("ADD_TODO", mongoTodo); - } - }); - }, - setNewTodo({ commit }, todo) { - // debugger - commit("SET_NEW_TODO", todo); - }, - clearNewTodo({ commit }) { - commit("CLEAR_NEW_TODO"); - }, - clearTodos({ commit, state }, all) { - // 1 fire and forget or - const deleteStuff = id => { - axios.delete(config.todoEndpoint + "/" + id).then(data => { - console.info("INFO - item " + id + " deleted", data); - }); - }; - - if (all) { - state.todos.map(todo => { - deleteStuff(todo._id); - }); - commit("CLEAR_ALL_TODOS"); - } else { - state.todos.map(todo => { - // axios remove all done by the id - if (todo.completed) { - deleteStuff(todo._id); - } - }); - commit("CLEAR_ALL_DONE_TODOS"); - } - // 2 return array of promises and resolve all - }, - /* eslint: ignore */ - updateTodo({ commit, state }, { id, important }) { - let i = state.todos.findIndex(todo => todo._id === id); - if (important) { - // TODO - add commit imporant here! - commit("MARK_TODO_IMPORTANT", i); - } else { - commit("MARK_TODO_COMPLETED", i); - } - // Fire and forget style backend update ;) - return axios - .put(config.todoEndpoint + "/" + state.todos[i]._id, state.todos[i]) - .then(() => { - console.info("INFO - item " + id + " updated"); - }); - } -}; - -``` - - - -21. Finally, let's implement the `mutation` for our feature. Again, starting with the tests... Open the `tests/unit/javascript/mutations.spec.js` to find our skeleton tests at the bottom of the file. Our mutation method is responsible for toggling the todo's `important` property between `true` and `false`. Let's implement the tests for this functionality by setting important to be true and calling the method expecting the inverse. Then let's set it to false and call the method expecting the inverse. Add the expectations below the `// TODO - test goes here!` comment as done previously. - -πŸ“ todolist/tests/unit/javascript/mutations.spec.js - - - -#### ** Important Part ** - -```javascript - it("it should MARK_TODO_IMPORTANT as false", () => { - state.todos = importantTodos; - // TODO - test goes here! - mutations.MARK_TODO_IMPORTANT(state, 0); - expect(state.todos[0].important).toBe(false); - }); - - it("it should MARK_TODO_IMPORTANT as true", () => { - state.todos = importantTodos; - // TODO - test goes here! - state.todos[0].important = false; - mutations.MARK_TODO_IMPORTANT(state, 0); - expect(state.todos[0].important).toBe(true); - }); -``` - -#### ** Entire File ** - -```javascript -import mutations from "@/store/mutations"; - -let state; -const todo = { - completed: true, - title: "testing sucks" -}; -const newTodo = "biscuits"; -const doneTodos = [ - { - completed: true, - title: "testing sucks" - }, - { - completed: false, - title: "easy testing is fun" - } -]; -const importantTodos = [ - { - completed: true, - title: "testing sucks", - important: true - } -]; - -describe("Mutation tests", () => { - beforeEach(() => { - state = {}; - }); - it("sets the loading to true", () => { - mutations.SET_LOADING(state, true); - expect(state.loading).toBe(true); - }); - it("sets the loading to false", () => { - mutations.SET_LOADING(state, false); - expect(state.loading).toBe(false); - }); - - it("sets all SET_TODOS", () => { - mutations.SET_TODOS(state, [todo]); - expect(state.todos.length).toBe(1); - }); - - it("SET_NEW_TODO", () => { - mutations.SET_NEW_TODO(state, newTodo); - expect(state.newTodo).toEqual(newTodo); - }); - - it("ADD_TODO", () => { - state.todos = []; - mutations.ADD_TODO(state, todo); - expect(state.todos.length).toBe(1); - }); - - it("CLEAR_NEW_TODO", () => { - state.newTodo = newTodo; - mutations.CLEAR_NEW_TODO(state, newTodo); - expect(state.newTodo).toEqual(""); - }); - - it("CLEAR_NEW_TODO", () => { - state.newTodo = newTodo; - mutations.CLEAR_NEW_TODO(state); - expect(state.newTodo).toEqual(""); - }); - - it("CLEAR_ALL_DONE_TODOS", () => { - state.todos = doneTodos; - mutations.CLEAR_ALL_DONE_TODOS(state); - expect(state.todos.length).toBe(1); - expect(state.todos[0].completed).toBe(false); - }); - - it("CLEAR_ALL_TODOS", () => { - state.todos = doneTodos; - mutations.CLEAR_ALL_TODOS(state); - expect(state.todos.length).toBe(0); - }); - - it("MARK_TODO_COMPLETED", () => { - state.todos = doneTodos; - mutations.MARK_TODO_COMPLETED(state, 0); - expect(state.todos[0].completed).toBe(false); - // check the reversy! - mutations.MARK_TODO_COMPLETED(state, 0); - expect(state.todos[0].completed).toBe(true); - }); - - it("it should MARK_TODO_IMPORTANT as false", () => { - state.todos = importantTodos; - // TODO - test goes here! - mutations.MARK_TODO_IMPORTANT(state, 0); - expect(state.todos[0].important).toBe(false); - }); - - it("it should MARK_TODO_IMPORTANT as true", () => { - state.todos = importantTodos; - // TODO - test goes here! - state.todos[0].important = false; - mutations.MARK_TODO_IMPORTANT(state, 0); - expect(state.todos[0].important).toBe(true); - }); -}); - -``` - - - -22. With our tests running and failing, let's implement the feature to their spec. Open the `src/store/mutations.js` and add another function called `MARK_TODO_IMPORTANT` below the `MARK_TODO_COMPLETED` to toggle `todo.important` between true and false. - -NOTE - add a `,` at the end of the `MARK_TODO_COMPLETED(){}` function call. - -πŸ“ todolist/src/store/mutations.js - - - -#### ** Important Part ** - -```javascript - MARK_TODO_IMPORTANT(state, index) { - console.log("INFO - MARK_TODO_IMPORTANT"); - state.todos[index].important = !state.todos[index].important; - } -``` - -#### ** Entire File ** - -```javascript -export default { - SET_LOADING(state, bool) { - console.log("INFO - Setting loading wheel"); - state.loading = bool; - }, - SET_TODOS(state, todos) { - console.log("INFO - Setting todos"); - state.todos = todos; - }, - SET_NEW_TODO(state, todo) { - console.log("INFO - Setting new todo"); - state.newTodo = todo; - }, - ADD_TODO(state, todo) { - console.log("INFO - Add todo", todo); - state.todos.push(todo); - }, - CLEAR_NEW_TODO(state) { - console.log("INFO - Clearing new todo"); - state.newTodo = ""; - }, - CLEAR_ALL_DONE_TODOS(state) { - console.log("INFO - Clearing all done todos"); - state.todos = state.todos.filter(obj => obj.completed === false); - }, - CLEAR_ALL_TODOS(state) { - console.log("INFO - Clearing all todos"); - state.todos = []; - }, - MARK_TODO_COMPLETED(state, index) { - console.log("INFO - MARK_TODO_COMPLETED"); - state.todos[index].completed = !state.todos[index].completed; - }, - MARK_TODO_IMPORTANT(state, index) { - console.log("INFO - MARK_TODO_IMPORTANT"); - state.todos[index].important = !state.todos[index].important; - } -}; -``` - - - -![mark-todo-important](../images/exercise3/mark-todo-important.png) - -23. All our tests should now be passing. On the watch tab where they are running, hit `a` to re-run all tests and update any snapshots with `u` if needed. - -24. With all our tests now passing, let's commit our code. On the terminal, run - -```bash -git add . -``` -```bash -git commit -m "Implementing the store and actions" -``` -```bash -git push -``` - -25. Before running a build in Jenkins, let's add our tests and code to the develop branch. Ask your neighbour to review your code changes and if they approve; merge them to Develop! (If you're feeling adventurous - raise a PR through GitLab and have someone on your desk peer review it!) - -```bash -git checkout develop -``` -```bash -git merge feature/important-flag -``` - -When the editor screen appears in the terminal after running the merge, type `:q` and hit enter to quit the editor. - -```bash -git push --all -``` + ```bash + git push --all + ``` #### 2c - Create todolist e2e tests -> _Using [Nightwatch.js](http://nightwatchjs.org/) We will write a reasonably simple e2e test to test the functionality of the feature we just implemented._ +Using [Nightwatch.js](http://nightwatchjs.org/) We will write a simple e2e test to test the functionality of the feature we just implemented. -1. Firstly we need to create an e2e spec test file in the correct place. +1. First, we need to create an e2e test file. -```bash -touch tests/e2e/specs/importantFlag.js -``` + ```bash + touch tests/e2e/specs/importantFlag.js + ``` -2. Open this new file in your code editor and set out the initial blank template for an e2e test as below: +1. Open this new file in your code editor and create an initial template for an e2e test as shown below: -πŸ“ todolist/tests/e2e/specs/importantFlag.js + πŸ“ todolist/tests/e2e/specs/importantFlag.js -```javascript - module.exports = { - "Testing important flag setting": browser => { + ```javascript + module.exports = { + "Testing important flag setting": browser => { + } } - }; -``` - -![if-e2e-step1](../images/exercise3/if-e2e-step1.png) + ``` -3. Now get the test to access the todos page and wait for it to load. The url can be taken from `process.env.VUE_DEV_SERVER_URL` +1. Now, get the test to access the todos page and wait for it to load. The url can be taken from `process.env.VUE_DEV_SERVER_URL` πŸ“ todolist/tests/e2e/specs/importantFlag.js @@ -2907,36 +2435,32 @@ touch tests/e2e/specs/importantFlag.js ```javascript browser - .url(process.env.VUE_DEV_SERVER_URL + '/#/todo') - .waitForElementVisible('body', 5000); + .url(process.env.VUE_DEV_SERVER_URL + "/#/todo") + .waitForElementVisible("body", 5000); ``` #### ** Entire File ** ```javascript module.exports = { - "Testing important flag setting": browser => { + "Testing important flag setting": (browser) => { browser - .url(process.env.VUE_DEV_SERVER_URL + '/#/todo') - .waitForElementVisible('body', 5000); - } + .url(process.env.VUE_DEV_SERVER_URL + "/#/todo") + .waitForElementVisible("body", 5000); + }, }; ``` -![if-e2e-step2](../images/exercise3/if-e2e-step2.png) +1. Write code to do the following: -4. Write code to do the following: - * Click the clear all button and then enter a value in the textbox to create a new item. - * Assert that an 'important flag' exists (the button to set important) - * Assert that a red flag does not exist. - * Click the important flag and check whether the flag has turned red. + - Click the clear all button and then enter a value in the textbox to create a new item. + - Assert that an 'important flag' exists (the button to set important) + - Assert that a red flag does not exist. + - Click the important flag and check whether the flag has turned red. -5. You will need to reference the clear all button from the test code. We will therefore have to go to `src/components/XofYItems.vue` and add `id="clear-all"` to the clear all button. - - - ![if-e2e-step3a](../images/exercise3/if-e2e-step3a.png) +1. You will need to reference the clear all button from the test code. We will therefore have to go to `src/components/XofYItems.vue` and add `id="clear-all"` to the clear all button. πŸ“ src/components/XofYItems.vue @@ -2945,75 +2469,40 @@ module.exports = { #### ** Important Part ** ```html - ``` #### ** Entire File ** ```html - - - - - -