Compare commits
54 Commits
v3.0
...
2807f7289c
Author | SHA1 | Date | |
---|---|---|---|
|
2807f7289c | ||
|
79da7a9dc9 | ||
616f2bf199 | |||
d7969fed3d
|
|||
2d592123d1
|
|||
6c56c4cf3b
|
|||
f6610ba524
|
|||
32e2d37dd3
|
|||
|
b443a56524 | ||
|
276e4ced5c | ||
140dbb1989 | |||
355865586f
|
|||
b170eecb16
|
|||
c308ffd2dd
|
|||
7da76417f7
|
|||
697de24eab | |||
b3f3ce0b16
|
|||
|
5c7bbac1c5 | ||
|
75b8f565bc | ||
4c7a0bd88f | |||
|
c81193364d
|
||
|
394cc91ec6 | ||
|
befd66898d | ||
|
a3b4f90135 | ||
|
fbea64d742 | ||
|
8703d85b1f | ||
|
a59707eb23 | ||
|
c1a195f0f8 | ||
|
a718be6da8 | ||
|
ab278cc9b3 | ||
|
5e9f03e0f7 | ||
bcec88f930
|
|||
188a4725e6
|
|||
8604e3e984
|
|||
4e27f8c9d9
|
|||
5e926d7674
|
|||
eda58ace70
|
|||
|
9897526b14 | ||
|
62090041f5 | ||
|
055401ed53 | ||
|
9c496e73bb | ||
|
df1db2fd97 | ||
|
570a0d3cea | ||
|
89e66a8b58 | ||
|
83d70c4fc5 | ||
|
3d6abfa9bc | ||
|
528da91ed4 | ||
|
ea40810fa3 | ||
|
0108bb25ed | ||
de8a0ab688
|
|||
8590c7a715 | |||
|
be3db6e605 | ||
|
34e24304fa | ||
|
ee2f80d670 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,3 +5,7 @@ __pycache__/
|
|||||||
.devcontainer/
|
.devcontainer/
|
||||||
|
|
||||||
src/.pdm-python
|
src/.pdm-python
|
||||||
|
src/htmlcov
|
||||||
|
src/.env
|
||||||
|
|
||||||
|
.env
|
||||||
|
106
.gitlab-ci.yml
106
.gitlab-ci.yml
@@ -1,34 +1,50 @@
|
|||||||
|
variables:
|
||||||
|
DOCKER_IMAGE_TEST: registry.forge.hefr.ch/klagarge/mse2425-grp09/python-pdm:latest
|
||||||
|
DOCKER_IMAGE_APP: registry.forge.hefr.ch/klagarge/mse2425-grp09/devsecops-app:latest
|
||||||
|
|
||||||
|
default:
|
||||||
|
image: $DOCKER_IMAGE_TEST
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build-docker
|
||||||
|
- lint
|
||||||
|
- test
|
||||||
|
|
||||||
|
.setup_env: &setup_env
|
||||||
build job:
|
before_script:
|
||||||
stage: build
|
|
||||||
script:
|
|
||||||
- cd src
|
- cd src
|
||||||
- apt-get update -qy
|
- cp -r /app/__pypackages__ .
|
||||||
- apt-get install -y python3-dev python3-pip python3.12-venv curl
|
- export "PYTHONPATH=/builds/Klagarge/mse2425-grp09/src:/builds/Klagarge/mse2425-grp09/src/__pypackages__/3.9/lib"
|
||||||
- python3 -V
|
- export "PATH=/builds/Klagarge/mse2425-grp09/src/__pypackages__/3.9/bin:$PATH"
|
||||||
# - pip3 install --break-system-packages -r requirements.txt
|
- export "FLASK_APP=app"
|
||||||
- curl -sSL https://pdm-project.org/install-pdm.py | python3 -
|
|
||||||
- export PATH=/root/.local/bin:$PATH
|
test job:
|
||||||
- pdm install
|
stage: test
|
||||||
|
<<: *setup_env
|
||||||
|
script:
|
||||||
|
# Set environment variables for the tests
|
||||||
|
- export FLASK_SECRET_KEY=$FLASK_SECRET_KEY
|
||||||
|
|
||||||
# launch tests
|
# launch tests
|
||||||
- export PYTHONPATH=.
|
|
||||||
- export FLASK_APP=app
|
|
||||||
- pdm run pytest tests --cov --cov-report term --cov-report html
|
- pdm run pytest tests --cov --cov-report term --cov-report html
|
||||||
|
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- src/htmlcov/
|
- src/htmlcov/
|
||||||
|
|
||||||
|
lint job:
|
||||||
|
stage: lint
|
||||||
|
<<: *setup_env
|
||||||
|
dependencies: []
|
||||||
|
script:
|
||||||
|
- pdm run flake8 --config=../tox.ini
|
||||||
|
allow_failure: true # Linter can fail, fixing it is for now outside of the projects scope
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: build
|
stage: test
|
||||||
dependencies:
|
dependencies:
|
||||||
- build job
|
- test job
|
||||||
needs: ["build job"]
|
needs: ["test job"]
|
||||||
script:
|
script:
|
||||||
- mv src/htmlcov/ public/
|
- mv src/htmlcov/ public/
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -37,3 +53,53 @@ pages:
|
|||||||
expire_in: 7 days
|
expire_in: 7 days
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
# This job runs only when Dockerfile changes
|
||||||
|
docker-build-test:
|
||||||
|
image: docker:latest
|
||||||
|
stage: build-docker
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
script:
|
||||||
|
- docker build -t $DOCKER_IMAGE_TEST -f Dockerfile .
|
||||||
|
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
|
||||||
|
- docker push $DOCKER_IMAGE_TEST
|
||||||
|
rules:
|
||||||
|
- if: $GITLAB_CI == 'false' # Only run in GitLab CI
|
||||||
|
when: never
|
||||||
|
- changes:
|
||||||
|
- Dockerfile
|
||||||
|
- src/pyproject.toml
|
||||||
|
- src/pdm.lock
|
||||||
|
|
||||||
|
docker-build-app:
|
||||||
|
image: docker:latest
|
||||||
|
stage: build-docker
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
script:
|
||||||
|
- docker build -t $DOCKER_IMAGE_APP -f src/Dockerfile .
|
||||||
|
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
|
||||||
|
- docker push $DOCKER_IMAGE_APP
|
||||||
|
|
||||||
|
include:
|
||||||
|
- template: Jobs/SAST.gitlab-ci.yml
|
||||||
|
|
||||||
|
dast:
|
||||||
|
stage: test
|
||||||
|
image: ghcr.io/zaproxy/zaproxy:stable
|
||||||
|
services:
|
||||||
|
- name: $DOCKER_IMAGE_APP
|
||||||
|
alias: app
|
||||||
|
script:
|
||||||
|
- echo "Waiting for the app to start on http://app:5000"
|
||||||
|
- timeout 60 bash -c 'until curl -s http://app:5000; do echo "Waiting..."; sleep 3; done'
|
||||||
|
- zap-full-scan.py -t http://app:5000 -I
|
||||||
|
|
||||||
|
gitleaks:
|
||||||
|
stage: test
|
||||||
|
image:
|
||||||
|
name: zricethezav/gitleaks:latest
|
||||||
|
entrypoint: [""]
|
||||||
|
script:
|
||||||
|
- gitleaks dir -v --redact=75 .
|
||||||
|
23
.pre-commit-config.yaml
Normal file
23
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
rev: '7.2.0' # Use the latest stable version
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies: []
|
||||||
|
args: [--config=tox.ini] # Use the same config as in CI
|
||||||
|
|
||||||
|
- repo: https://github.com/pypa/pip-audit
|
||||||
|
rev: v2.9.0
|
||||||
|
hooks:
|
||||||
|
- id: pip-audit
|
||||||
|
args: ["./src"]
|
||||||
|
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.24.3
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
|
|
||||||
|
ci:
|
||||||
|
# Leave pip-audit to only run locally and not in CI
|
||||||
|
# pre-commit.ci does not allow network calls
|
||||||
|
skip: [pip-audit]
|
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
LABEL org.opencontainers.image.authors="remi.heredero@hevs.ch"
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
pip install --no-cache-dir -U pdm && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
ENV PATH="/root/.local/bin:$PATH" \
|
||||||
|
PDM_USE_VENV=false
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY src/pyproject.toml src/pdm.lock ./
|
||||||
|
|
||||||
|
RUN pdm config python.use_venv false && \
|
||||||
|
pdm install -G:all
|
||||||
|
|
||||||
|
ENV PYTHONPATH="/app/__pypackages__/3.9/lib" \
|
||||||
|
PATH="/app/__pypackages__/3.9/bin:$PATH"
|
BIN
docs/figures/OWASP-ZAP.png
Normal file
BIN
docs/figures/OWASP-ZAP.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
docs/figures/SAST-report.png
Normal file
BIN
docs/figures/SAST-report.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
@@ -6,3 +6,15 @@
|
|||||||
- **Q1.2**: The secret key for flask is hard coded. Is this good practice? What are the dangers? How could this be fixed?
|
- **Q1.2**: The secret key for flask is hard coded. Is this good practice? What are the dangers? How could this be fixed?
|
||||||
- **Q1.3**: Give a short description of *Linter*. Integrate a basic linter like [Flake8](https://flake8.pycqa.org/en/latest/) or [Ruff](https://github.com/astral-sh/ruff) in the existing CI/CD pipeline
|
- **Q1.3**: Give a short description of *Linter*. Integrate a basic linter like [Flake8](https://flake8.pycqa.org/en/latest/) or [Ruff](https://github.com/astral-sh/ruff) in the existing CI/CD pipeline
|
||||||
- **Q1.4 (optional)**: The run of the current CI/CD pipeline takes some time. Especially the time to setup the docker with the update and installation of all the dependencies is quite time consuming compared to the real testing time. Do you see any alternatives to speed up this process? Describe and try to implement it in your pipeline.
|
- **Q1.4 (optional)**: The run of the current CI/CD pipeline takes some time. Especially the time to setup the docker with the update and installation of all the dependencies is quite time consuming compared to the real testing time. Do you see any alternatives to speed up this process? Describe and try to implement it in your pipeline.
|
||||||
|
|
||||||
|
# Answers - Part 1
|
||||||
|
## Q1.2
|
||||||
|
- It's a very bad practice. The secret key will be exposed in the codebase and can be easily accessed by anyone who has access to the codebase. This can lead to security vulnerabilities and compromise the integrity of the application.
|
||||||
|
- To fix this, you can use environment variables to store the secret key.
|
||||||
|
|
||||||
|
## Q1.3
|
||||||
|
- A linter is a tool to statically analyse code for readability and improving code quality. It is usually executed from a standalone tool.
|
||||||
|
- It is used to check for errors, vulns, code smells or general issues but also to enforce a coding style over the whole project.
|
||||||
|
|
||||||
|
## Q1.4
|
||||||
|
The minimum is to change the image on the CI to put an Alpine or a basic python image. Another option (which we implemented) is to create a custom Docker image that includes all the necessary dependencies for the application. This can significantly reduce the time required to set up the environment and speed up the CI/CD pipeline.
|
||||||
|
@@ -5,3 +5,42 @@
|
|||||||
- **Q2.1**: Every commit triggers the CI/CD pipeline. Find out a way to trigger the pipeline only if specific commits (e.g. commit in a development branch) are made. Where can this be configured. Describe your solution and implement it in your pipeline.
|
- **Q2.1**: Every commit triggers the CI/CD pipeline. Find out a way to trigger the pipeline only if specific commits (e.g. commit in a development branch) are made. Where can this be configured. Describe your solution and implement it in your pipeline.
|
||||||
- **Q2.2**: Take the [CIS controls](./CIS_Controls_v8_Online.22.02.pdf) and give some examples (minimum 5) of controls from this standard that are not or not enough implemented in the calculator app. Provide a short description and a possible remediation. Implement at least two of the controls in the app / pipeline.
|
- **Q2.2**: Take the [CIS controls](./CIS_Controls_v8_Online.22.02.pdf) and give some examples (minimum 5) of controls from this standard that are not or not enough implemented in the calculator app. Provide a short description and a possible remediation. Implement at least two of the controls in the app / pipeline.
|
||||||
- **Q2.3 (optional)**: The linter from question 1.3 is a good start. It is only executed in your pipeline. But what if you would also integrate it directly in your local development environment (e.g. IDE)? Can you do the linting before you commit? Describe your solution and implement it in your (local) pipeline. Describe the advantages and disadvantages of this approach.
|
- **Q2.3 (optional)**: The linter from question 1.3 is a good start. It is only executed in your pipeline. But what if you would also integrate it directly in your local development environment (e.g. IDE)? Can you do the linting before you commit? Describe your solution and implement it in your (local) pipeline. Describe the advantages and disadvantages of this approach.
|
||||||
|
|
||||||
|
# Answers - Part 2
|
||||||
|
## Q2.1
|
||||||
|
Solution is to add a `rule` section to add condition to trigger the pipeline. It's what is implemented for the `docker-build` job. Another option is to use an `only` section to trigger the pipeline only if the change is made in a specific branch. It's what is implemented for the `pages` job.
|
||||||
|
|
||||||
|
## Q2.2
|
||||||
|
|
||||||
|
### Example 1 - 3.6 Encrypt Data on End-User Devices
|
||||||
|
|
||||||
|
**description**
|
||||||
|
Sensible data is everywhere. It is also on the end user's device. It is primordial to keep it secure.
|
||||||
|
|
||||||
|
**mitigation**
|
||||||
|
There is multiple ways to secure data. CIS suggests the following : Windows BitLocker®, Apple FileVault®, Linux® dm-crypt.
|
||||||
|
|
||||||
|
### Example 2 - 4.3 Configure Automatic Session Locking on Enterprise Assets
|
||||||
|
|
||||||
|
**description**
|
||||||
|
A logged in computer doesn't check permanantly for the user's identity. A user could by mistake leave his computer open and logged in, give way for anyone ill-intentionned with physical access to use the computer with it's permissions.
|
||||||
|
|
||||||
|
**mitigation**
|
||||||
|
Forcing an auto-logout after a few minutes.
|
||||||
|
|
||||||
|
### Example 3 - 5.2 Use Unique Passwords
|
||||||
|
|
||||||
|
**description**
|
||||||
|
If a user uses the same password everywhere, it only needs one to get compromised and everything is equally compromised.
|
||||||
|
|
||||||
|
**mitigation**
|
||||||
|
Usage of unique passwords and for users use 2FA at least.
|
||||||
|
|
||||||
|
### Example 4 & 5 with implementation
|
||||||
|
|
||||||
|
Unfortunately, due to the amount of work we both had, from work and from school, we didn't have enough time to do those last two points.
|
||||||
|
We did the rest (except for optionals) though.
|
||||||
|
|
||||||
|
## Q2.3
|
||||||
|
|
||||||
|
We can use a pre-commit that runs the linter before committing. This ensures that the code is linted before it is committed, which can help catch errors and improve code quality. However, this approach can be time-consuming and may require additional setup.
|
||||||
|
35
docs/questions-part3.md
Normal file
35
docs/questions-part3.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Questions
|
||||||
|
|
||||||
|
## Part 3
|
||||||
|
|
||||||
|
- **Q3.1**: Setup your CI/CD pipeline with an additional SAST solution. I propose that you use `semgrep` for this task. Get your inspiration here: https://semgrep.dev/for/gitlab and https://docs.gitlab.com/ee/user/application_security/sast/
|
||||||
|
- **Q3.2**: Describe the found problems (alerts) in the `calculator app` (in the original code, git tag `v3.0`)
|
||||||
|
- **Q3.3**: Install DAST OWASP ZAP on your host or in a Docker. Play with OWASP ZAP, analyze the calculator code
|
||||||
|
- **Q3.4**: Implement a DAST solution in your pipeline. Get some inspiration here https://docs.gitlab.com/ee/user/application_security/dast/ . Describe what you have integrated in your pipeline. *Note: you must ensure that your application is running while you are testing!*
|
||||||
|
- **Q3.5 (optional)**: Normally, the provided code has some bugs, which are discovered by SAST solution. Describe the found bugs (in the original code, git tag `v3.0`) and provide solution to remediate the problems. Indicate which commit/tag contains the corrected code
|
||||||
|
- **Q3.6 (optional)**: Describe the found bugs (in the original code, git tag `v3.0`) with DAST and provide solution to remediate the problems. Indicate which commit/tag contains the corrected code. Do corrections only in the provided code (no libraries)
|
||||||
|
|
||||||
|
|
||||||
|
# Answers - Part 3
|
||||||
|
|
||||||
|
## Q3.2
|
||||||
|
|
||||||
|
For some reasons, semgrep works locally, but not on GitLab. Here is the report when runned locally.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Q3.3
|
||||||
|
|
||||||
|
After performing a scan, we can see a few alerts as seen on this screenshot :
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Q3.4
|
||||||
|
The integrate DAST in Github doesn't work on our version, we need the _Ultimate_ version of GitLab selfhosted.
|
||||||
|
|
||||||
|
We create a new Docker image for the application. This image auto launch the flask app when the container is started.
|
||||||
|
|
||||||
|
We used this image as a service for the DAST stage on our CI.
|
||||||
|
The stage use zaproxy to test the application. Warning do not return wailure, so the stage pass if no error is found by the OWASP ZAP.
|
||||||
|
|
||||||
|
We don't understand why the stage fail when we try to provide the html report as artifact. So if the stage fail, we can see the error in the logs.
|
26
docs/questions-part4.md
Normal file
26
docs/questions-part4.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Questions
|
||||||
|
|
||||||
|
## Part 4
|
||||||
|
|
||||||
|
- **Q4.1**: Often secrets are committed in a repository. Different research tools exist and help to detect this kind of dangerous forgotten credentials. Integrate a check in your pipeline for these kinds of problems. Have a look at <https://github.com/zricethezav/gitleaks>. What kind of leaked secrets can you find in the git repo? Did the tool not find something that it should have found? Why? What possibilities exist to prevent this kind of leakage?
|
||||||
|
- **Q4.2**: Try to find any possible problems in our used libraries (e.g. flask). The `pyproject.toml` describes all the additional libraries used by the application. You can use a dependency scanning (have a look here: <https://docs.gitlab.com/ee/user/application_security/dependency_scanning/>) to see if all imported libraries are safe. Do you find any problems? Integrate the scanning in your pipeline.
|
||||||
|
- **Q4.3 (optional)**: API Fuzzing (and other kinds of DAST) is described at this page: <https://docs.gitlab.com/ee/user/application_security/api_fuzzing/>. Choose one of the different description possibilities for your *calculator* API. Integrate it in your pipeline.
|
||||||
|
|
||||||
|
|
||||||
|
# Answers - Part 4
|
||||||
|
## Q4.1
|
||||||
|
GitLeaks can find strings like API keys, passwords, and other sensitive information that might be accidentally committed to a repository. This tool can only recognize them if they look like sensitive information.
|
||||||
|
|
||||||
|
The scan of the git repository didn't detect the previous flask key because it was not in the format that GitLeaks recognizes.
|
||||||
|
|
||||||
|
Usually, the best practice is to use environment variables to store sensitive information. This way, the information is not exposed in the code.
|
||||||
|
|
||||||
|
## Q4.2
|
||||||
|
The Dependency scanning tool from GitLab linked by the teacher in the exercise cannot be used as it is limited to Gitlab Ultimate. I am looking into using an open-source solution
|
||||||
|
|
||||||
|
After using three different scanning tools, no known vulnerabilities were found.
|
||||||
|
|
||||||
|
Tools used :
|
||||||
|
- pyscan
|
||||||
|
- safety
|
||||||
|
- pip-audit
|
9
misc/docker-compose.yml
Normal file
9
misc/docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
container_name: nginx-1.20.0
|
||||||
|
image: nginx:1.20.0
|
||||||
|
volumes:
|
||||||
|
- .:/usr/share/nginx/html:ro
|
||||||
|
|
@@ -1 +1 @@
|
|||||||
Hello
|
<h1>Hello class, TSM_Cybersec 2025</h1>
|
||||||
|
4
misc/start_juiceshop.sh
Executable file
4
misc/start_juiceshop.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker run --rm -p 3000:3000 bkimminich/juice-shop
|
||||||
|
|
@@ -1,2 +1,5 @@
|
|||||||
[run]
|
[run]
|
||||||
omit=/usr/lib/python3/dist-packages/*
|
omit=
|
||||||
|
/usr/lib/python3/dist-packages/*
|
||||||
|
./__pypackages__
|
||||||
|
./src/__pypackages__
|
1
src/.env.template
Normal file
1
src/.env.template
Normal file
@@ -0,0 +1 @@
|
|||||||
|
FLASK_SECRET_KEY=
|
31
src/Dockerfile
Normal file
31
src/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
LABEL org.opencontainers.image.authors="remi.heredero@hevs.ch"
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
pip install --no-cache-dir -U pdm && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
ENV PATH="/root/.local/bin:$PATH" \
|
||||||
|
PDM_USE_VENV=false
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY src/pyproject.toml src/pdm.lock ./
|
||||||
|
|
||||||
|
RUN pdm config python.use_venv false && \
|
||||||
|
pdm install
|
||||||
|
|
||||||
|
ENV PYTHONPATH="/app/__pypackages__/3.9/lib" \
|
||||||
|
PATH="/app/__pypackages__/3.9/bin:$PATH"
|
||||||
|
|
||||||
|
############################################################
|
||||||
|
# Everything above should be imported from the test image, #
|
||||||
|
# but GitLab can't pull it, so I copy-paste the content #
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
COPY src ./
|
||||||
|
|
||||||
|
ENV FLASK_RUN_HOST=0.0.0.0
|
||||||
|
|
||||||
|
CMD ["pdm", "run", "flask"]
|
33
src/app.py
33
src/app.py
@@ -18,7 +18,7 @@
|
|||||||
from flask import request, Flask, url_for, render_template, redirect
|
from flask import request, Flask, url_for, render_template, redirect
|
||||||
import operators
|
import operators
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
__author__ = 'Michael Mäder'
|
__author__ = 'Michael Mäder'
|
||||||
__date__ = "2025-03-10"
|
__date__ = "2025-03-10"
|
||||||
@@ -36,7 +36,8 @@ A little web application that offers API calls for arithmetic operations
|
|||||||
# creation of the Flask application
|
# creation of the Flask application
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
app.config['SECRET_KEY'] = 'the-best-secret-ever' # super secure key against CSRF attacks
|
# super secure key against CSRF attacks
|
||||||
|
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY')
|
||||||
|
|
||||||
# global variable containing the name of the login user
|
# global variable containing the name of the login user
|
||||||
global_data = {'username': 'no_user'}
|
global_data = {'username': 'no_user'}
|
||||||
@@ -45,30 +46,44 @@ global_data = {'username': 'no_user'}
|
|||||||
# incrementation route
|
# incrementation route
|
||||||
@app.route('/inc')
|
@app.route('/inc')
|
||||||
def plus_one():
|
def plus_one():
|
||||||
x = int(request.args.get('x', 1))
|
try:
|
||||||
|
x = int(request.args.get('x', 1))
|
||||||
|
except ValueError:
|
||||||
|
return json.dumps({"error": "Type error"})
|
||||||
|
|
||||||
return json.dumps({'x': operators.addition(x, 1)})
|
return json.dumps({'x': operators.addition(x, 1)})
|
||||||
|
|
||||||
|
|
||||||
# addition route, the parameters will be passed with 'x' and 'y'
|
# addition route, the parameters will be passed with 'x' and 'y'
|
||||||
@app.route('/add')
|
@app.route('/add')
|
||||||
def plus_y():
|
def plus_y():
|
||||||
x = int(request.args.get('x', 1))
|
try:
|
||||||
y = int(request.args.get('y', 1))
|
x = int(request.args.get('x', 1))
|
||||||
|
y = int(request.args.get('y', 1))
|
||||||
|
except ValueError:
|
||||||
|
return json.dumps({"error": "Type error"})
|
||||||
return json.dumps({'result': operators.addition(x, y)})
|
return json.dumps({'result': operators.addition(x, y)})
|
||||||
|
|
||||||
|
|
||||||
# multiplication route, the parameters will be passed with 'x' and 'y'
|
# multiplication route, the parameters will be passed with 'x' and 'y'
|
||||||
@app.route('/mul')
|
@app.route('/mul')
|
||||||
def multiply_y():
|
def multiply_y():
|
||||||
x = int(request.args.get('x', 1))
|
try:
|
||||||
y = int(request.args.get('y', 1))
|
x = int(request.args.get('x', 1))
|
||||||
|
y = int(request.args.get('y', 1))
|
||||||
|
except ValueError:
|
||||||
|
return json.dumps({"error": "Type error"})
|
||||||
return json.dumps({'result': operators.multiplication(x, y)})
|
return json.dumps({'result': operators.multiplication(x, y)})
|
||||||
|
|
||||||
|
|
||||||
# division route, the parameters will be passed with 'x' and 'y'
|
# division route, the parameters will be passed with 'x' and 'y'
|
||||||
@app.route('/div')
|
@app.route('/div')
|
||||||
def division_y():
|
def division_y():
|
||||||
x = int(request.args.get('x', 1))
|
try:
|
||||||
y = int(request.args.get('y', 1))
|
x = int(request.args.get('x', 1))
|
||||||
|
y = int(request.args.get('y', 1))
|
||||||
|
except ValueError:
|
||||||
|
return json.dumps({"error": "Type error"})
|
||||||
return json.dumps({'result': operators.division(x, y)})
|
return json.dumps({'result': operators.division(x, y)})
|
||||||
|
|
||||||
|
|
||||||
|
@@ -42,6 +42,6 @@ def multiplication(x, y):
|
|||||||
# returns None if the divisor is zero
|
# returns None if the divisor is zero
|
||||||
# the result might be of float type
|
# the result might be of float type
|
||||||
def division(x, y):
|
def division(x, y):
|
||||||
if y==0:
|
if y == 0:
|
||||||
return None
|
return None
|
||||||
return x/y
|
return x/y
|
||||||
|
92
src/pdm.lock
generated
92
src/pdm.lock
generated
@@ -2,10 +2,10 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
groups = ["default"]
|
groups = ["default", "lint", "test"]
|
||||||
strategy = ["inherit_metadata"]
|
strategy = ["inherit_metadata"]
|
||||||
lock_version = "4.5.0"
|
lock_version = "4.5.0"
|
||||||
content_hash = "sha256:5a2be8939d6734b2295f420aee17c34be5958903eb13eba88b45213f3c4c0333"
|
content_hash = "sha256:f5c4e58e167316cb2440e9205a1e15474a3599463aa47c3c259b4009038166b0"
|
||||||
|
|
||||||
[[metadata.targets]]
|
[[metadata.targets]]
|
||||||
requires_python = ">3.11"
|
requires_python = ">3.11"
|
||||||
@@ -41,7 +41,7 @@ name = "colorama"
|
|||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
summary = "Cross-platform colored terminal text."
|
summary = "Cross-platform colored terminal text."
|
||||||
groups = ["default"]
|
groups = ["default", "test"]
|
||||||
marker = "sys_platform == \"win32\" or platform_system == \"Windows\""
|
marker = "sys_platform == \"win32\" or platform_system == \"Windows\""
|
||||||
files = [
|
files = [
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
@@ -53,7 +53,7 @@ name = "coverage"
|
|||||||
version = "7.6.12"
|
version = "7.6.12"
|
||||||
requires_python = ">=3.9"
|
requires_python = ">=3.9"
|
||||||
summary = "Code coverage measurement for Python"
|
summary = "Code coverage measurement for Python"
|
||||||
groups = ["default"]
|
groups = ["default", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"},
|
{file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"},
|
||||||
{file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"},
|
{file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"},
|
||||||
@@ -116,7 +116,7 @@ version = "7.6.12"
|
|||||||
extras = ["toml"]
|
extras = ["toml"]
|
||||||
requires_python = ">=3.9"
|
requires_python = ">=3.9"
|
||||||
summary = "Code coverage measurement for Python"
|
summary = "Code coverage measurement for Python"
|
||||||
groups = ["default"]
|
groups = ["default", "test"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"coverage==7.6.12",
|
"coverage==7.6.12",
|
||||||
"tomli; python_full_version <= \"3.11.0a6\"",
|
"tomli; python_full_version <= \"3.11.0a6\"",
|
||||||
@@ -177,6 +177,34 @@ files = [
|
|||||||
{file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"},
|
{file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.9.9"
|
||||||
|
summary = "Deprecated package"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"python-dotenv",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8"
|
||||||
|
version = "7.1.2"
|
||||||
|
requires_python = ">=3.8.1"
|
||||||
|
summary = "the modular source code checker: pep8 pyflakes and co"
|
||||||
|
groups = ["lint"]
|
||||||
|
dependencies = [
|
||||||
|
"mccabe<0.8.0,>=0.7.0",
|
||||||
|
"pycodestyle<2.13.0,>=2.12.0",
|
||||||
|
"pyflakes<3.3.0,>=3.2.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"},
|
||||||
|
{file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flask"
|
name = "flask"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
@@ -217,7 +245,7 @@ name = "iniconfig"
|
|||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "brain-dead simple config-ini parsing"
|
summary = "brain-dead simple config-ini parsing"
|
||||||
groups = ["default"]
|
groups = ["default", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
@@ -308,12 +336,23 @@ files = [
|
|||||||
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.7.0"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "McCabe checker, plugin for flake8"
|
||||||
|
groups = ["lint"]
|
||||||
|
files = [
|
||||||
|
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||||
|
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "24.2"
|
version = "24.2"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Core utilities for Python packages"
|
summary = "Core utilities for Python packages"
|
||||||
groups = ["default"]
|
groups = ["default", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||||
@@ -324,18 +363,40 @@ name = "pluggy"
|
|||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "plugin and hook calling mechanisms for python"
|
summary = "plugin and hook calling mechanisms for python"
|
||||||
groups = ["default"]
|
groups = ["default", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycodestyle"
|
||||||
|
version = "2.12.1"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Python style guide checker"
|
||||||
|
groups = ["lint"]
|
||||||
|
files = [
|
||||||
|
{file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"},
|
||||||
|
{file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyflakes"
|
||||||
|
version = "3.2.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "passive checker of Python programs"
|
||||||
|
groups = ["lint"]
|
||||||
|
files = [
|
||||||
|
{file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
|
||||||
|
{file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.3.5"
|
version = "8.3.5"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "pytest: simple powerful testing with Python"
|
summary = "pytest: simple powerful testing with Python"
|
||||||
groups = ["default"]
|
groups = ["default", "test"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"colorama; sys_platform == \"win32\"",
|
"colorama; sys_platform == \"win32\"",
|
||||||
"exceptiongroup>=1.0.0rc8; python_version < \"3.11\"",
|
"exceptiongroup>=1.0.0rc8; python_version < \"3.11\"",
|
||||||
@@ -354,7 +415,7 @@ name = "pytest-cov"
|
|||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
requires_python = ">=3.9"
|
requires_python = ">=3.9"
|
||||||
summary = "Pytest plugin for measuring coverage."
|
summary = "Pytest plugin for measuring coverage."
|
||||||
groups = ["default"]
|
groups = ["default", "test"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"coverage[toml]>=7.5",
|
"coverage[toml]>=7.5",
|
||||||
"pytest>=4.6",
|
"pytest>=4.6",
|
||||||
@@ -364,6 +425,17 @@ files = [
|
|||||||
{file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"},
|
{file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.0.1"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
||||||
|
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "werkzeug"
|
name = "werkzeug"
|
||||||
version = "3.1.3"
|
version = "3.1.3"
|
||||||
|
@@ -10,6 +10,7 @@ dependencies = [
|
|||||||
"pytest-cov>=4.1.0",
|
"pytest-cov>=4.1.0",
|
||||||
"Flask>=3.0.2",
|
"Flask>=3.0.2",
|
||||||
"flask-wtf>=1.2.1",
|
"flask-wtf>=1.2.1",
|
||||||
|
"dotenv>=0.9.9",
|
||||||
]
|
]
|
||||||
requires-python = ">3.11"
|
requires-python = ">3.11"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -23,3 +24,11 @@ distribution = false
|
|||||||
flask.cmd = "flask run -p 5000 --debug"
|
flask.cmd = "flask run -p 5000 --debug"
|
||||||
flask.env = {FLASK_ENV = "development"}
|
flask.env = {FLASK_ENV = "development"}
|
||||||
shell.cmd = "sh"
|
shell.cmd = "sh"
|
||||||
|
[dependency-groups]
|
||||||
|
test = [
|
||||||
|
"pytest>=8.3.5",
|
||||||
|
"pytest-cov>=6.0.0",
|
||||||
|
]
|
||||||
|
lint = [
|
||||||
|
"flake8>=7.1.2",
|
||||||
|
]
|
||||||
|
@@ -5,7 +5,7 @@ from app import app
|
|||||||
|
|
||||||
|
|
||||||
# the client here allow to use the app without running in live server
|
# the client here allow to use the app without running in live server
|
||||||
# see https://flask.palletsprojects.com/en/2.0.x/testing/#sending-requests-with-the-test-client
|
# see https://flask.palletsprojects.com/en/2.0.x/testing/#sending-requests-with-the-test-client # noqa: E501
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client():
|
def client():
|
||||||
app.config['TESTING'] = True
|
app.config['TESTING'] = True
|
||||||
|
@@ -39,27 +39,59 @@ def call(client, path, params):
|
|||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
return json.loads(response.data.decode('utf-8'))
|
return json.loads(response.data.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
# increment test 1
|
# increment test 1
|
||||||
def test_plus_one1(client):
|
def test_plus_one1(client):
|
||||||
result = call(client, '/inc', {'x': 2})
|
result = call(client, '/inc', {'x': 2})
|
||||||
assert result['x'] == 3
|
assert result['x'] == 3
|
||||||
|
|
||||||
|
|
||||||
# increment test 1
|
# increment test 1
|
||||||
def test_plus_one2(client):
|
def test_plus_one2(client):
|
||||||
result = call(client, '/inc', {'x': -2})
|
result = call(client, '/inc', {'x': -2})
|
||||||
assert result['x'] == -1
|
assert result['x'] == -1
|
||||||
|
|
||||||
|
|
||||||
# adding test with negative value
|
# adding test with negative value
|
||||||
def test_plus_y(client):
|
def test_plus_y(client):
|
||||||
result = call(client, '/add', {'x': -2, 'y': 7})
|
result = call(client, '/add', {'x': -2, 'y': 7})
|
||||||
assert result['result'] == 5
|
assert result['result'] == 5
|
||||||
|
|
||||||
|
|
||||||
# multiplication test
|
# multiplication test
|
||||||
def test_multiply(client):
|
def test_multiply(client):
|
||||||
result = call(client, '/mul', {'x': -2, 'y': 7})
|
result = call(client, '/mul', {'x': -2, 'y': 7})
|
||||||
assert result['result'] == -14
|
assert result['result'] == -14
|
||||||
|
|
||||||
|
|
||||||
# division test
|
# division test
|
||||||
def test_division(client):
|
def test_division(client):
|
||||||
result = call(client, '/div', {'x': 35, 'y': 7})
|
result = call(client, '/div', {'x': 35, 'y': 7})
|
||||||
assert result['result'] == 5
|
assert result['result'] == 5
|
||||||
|
|
||||||
|
|
||||||
|
# type tests
|
||||||
|
def test_type_inc(client):
|
||||||
|
result = call(client, '/inc', {'x': 'a'})
|
||||||
|
assert result["error"] == "Type error"
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_add(client):
|
||||||
|
result = call(client, '/add', {'x': 'a', 'y': 1})
|
||||||
|
assert result["error"] == "Type error"
|
||||||
|
result = call(client, '/add', {'x': 1, 'y': 'a'})
|
||||||
|
assert result["error"] == "Type error"
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_mul(client):
|
||||||
|
result = call(client, '/mul', {'x': 'a', 'y': 1})
|
||||||
|
assert result["error"] == "Type error"
|
||||||
|
result = call(client, '/mul', {'x': 1, 'y': 'a'})
|
||||||
|
assert result["error"] == "Type error"
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_div(client):
|
||||||
|
result = call(client, '/div', {'x': 'a', 'y': 1})
|
||||||
|
assert result["error"] == "Type error"
|
||||||
|
result = call(client, '/div', {'x': 1, 'y': 'a'})
|
||||||
|
assert result["error"] == "Type error"
|
||||||
|
Reference in New Issue
Block a user