├── .dockerignore
├── .env
├── .gitignore
├── Dockerfile
├── README.md
├── SECURITY.md
├── docker-compose.yml
├── frontend
├── .dockerignore
├── .gitignore
├── Dockerfile
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
└── src
│ ├── App.vue
│ ├── assets
│ └── logo.png
│ ├── components
│ ├── HelloWorld.vue
│ ├── Nav.vue
│ ├── auth
│ │ └── Login.vue
│ ├── posts
│ │ ├── EditPost.vue
│ │ ├── MyPosts.vue
│ │ ├── NewPost.vue
│ │ └── PublicPosts.vue
│ └── users
│ │ ├── EditUser.vue
│ │ ├── ListUser.vue
│ │ └── Register.vue
│ └── main.js
├── makefile
├── requirements.txt
└── src
├── __init__.py
├── api
├── __init__.py
├── auth_api.py
├── exceptions.py
├── post_api.py
└── user_api.py
├── core
├── __init__.py
├── config.py
└── db.py
├── main.py
├── models
├── __init__.py
├── post.py
└── user.py
├── serializers
├── __init__.py
├── comment.py
├── post.py
├── token.py
└── user.py
└── tests
├── __init__.py
├── conftest.py
└── endpoints
├── __init__.py
├── test_oauth.py
├── test_post.py
└── test_user.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.pyc
3 | *.pyo
4 | *.pyd
5 | .Python
6 | env
7 | venv
8 | pip-log.txt
9 | pip-delete-this-directory.txt
10 | .tox
11 | .coverage
12 | .coverage.*
13 | .cache
14 | nosetests.xml
15 | coverage.xml
16 | *,cover
17 | *.log
18 | .git
19 | .mypy_cache
20 | .pytest_cache
21 | .hypothesis
22 |
23 | .git
24 | .dockerignore
25 | .gitignore
26 | .github/*
27 | README.md
28 | Dockerfile
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### macOS ###
2 | # General
3 | .DS_Store
4 | .AppleDouble
5 | .LSOverride
6 |
7 | # Icon must end with two \r
8 | Icon
9 |
10 | # Thumbnails
11 | ._*
12 |
13 | # Files that might appear in the root of a volume
14 | .DocumentRevisions-V100
15 | .fseventsd
16 | .Spotlight-V100
17 | .TemporaryItems
18 | .Trashes
19 | .VolumeIcon.icns
20 | .com.apple.timemachine.donotpresent
21 |
22 | # Directories potentially created on remote AFP share
23 | .AppleDB
24 | .AppleDesktop
25 | Network Trash Folder
26 | Temporary Items
27 | .apdisk
28 |
29 | ### OSX ###
30 | # General
31 |
32 | # Icon must end with two \r
33 |
34 | # Thumbnails
35 |
36 | # Files that might appear in the root of a volume
37 |
38 | # Directories potentially created on remote AFP share
39 |
40 | ### Python ###
41 | # Byte-compiled / optimized / DLL files
42 | __pycache__/
43 | *.py[cod]
44 | *$py.class
45 |
46 | # C extensions
47 | *.so
48 |
49 | # Distribution / packaging
50 | .Python
51 | build/
52 | develop-eggs/
53 | dist/
54 | downloads/
55 | eggs/
56 | .eggs/
57 | lib/
58 | lib64/
59 | parts/
60 | sdist/
61 | var/
62 | wheels/
63 | pip-wheel-metadata/
64 | share/python-wheels/
65 | *.egg-info/
66 | .installed.cfg
67 | *.egg
68 | MANIFEST
69 |
70 | # PyInstaller
71 | # Usually these files are written by a python script from a template
72 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
73 | *.manifest
74 | *.spec
75 |
76 | # Installer logs
77 | pip-log.txt
78 | pip-delete-this-directory.txt
79 |
80 | # Unit test / coverage reports
81 | htmlcov/
82 | .tox/
83 | .nox/
84 | .coverage
85 | .coverage.*
86 | .cache
87 | nosetests.xml
88 | coverage.xml
89 | *.cover
90 | .hypothesis/
91 | .pytest_cache/
92 |
93 | # Translations
94 | *.mo
95 | *.pot
96 |
97 | # Scrapy stuff:
98 | .scrapy
99 |
100 | # Sphinx documentation
101 | docs/_build/
102 |
103 | # PyBuilder
104 | target/
105 |
106 | # pyenv
107 | .python-version
108 |
109 | # pipenv
110 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
111 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
112 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
113 | # install all needed dependencies.
114 | #Pipfile.lock
115 |
116 | # celery beat schedule file
117 | celerybeat-schedule
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Spyder project settings
123 | .spyderproject
124 | .spyproject
125 |
126 | # Rope project settings
127 | .ropeproject
128 |
129 | # Mr Developer
130 | .mr.developer.cfg
131 | .project
132 | .pydevproject
133 |
134 | # mkdocs documentation
135 | /site
136 |
137 | # mypy
138 | .mypy_cache/
139 | .dmypy.json
140 | dmypy.json
141 |
142 | # Pyre type checker
143 | .pyre/
144 |
145 | ### VirtualEnv ###
146 | pyvenv.cfg
147 | .env
148 | .venv
149 | env/
150 | venv/
151 | ENV/
152 | env.bak/
153 | venv.bak/
154 | pip-selfcheck.json
155 |
156 |
157 | # Editor
158 | .vscode
159 | .idea
160 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # pull official base image
2 | FROM python:3.8.5
3 |
4 | # set work directory
5 | WORKDIR /home/app/backend
6 |
7 | # set environment variables
8 | ENV PYTHONDONTWRITEBYTECODE 1
9 | ENV PYTHONUNBUFFERED 1
10 |
11 | # install dependencies
12 | RUN pip install --upgrade pip
13 | COPY ./requirements.txt .
14 | RUN pip install -r requirements.txt
15 |
16 | # copy project
17 | COPY src ./src
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blog API with FastAPI
2 |
3 | Simple project that I write using fastapi, motor, and umongo for the backend. VueJS on the frontend.
4 |
5 | ----------------------------
6 | ## Commands
7 |
8 | ### Run
9 | Run the app using the following command:
10 |
11 | ```
12 | docker-compose up -d --build
13 | # or
14 | make up
15 | ```
16 |
17 |
18 | ### Remove container
19 | remove container
20 |
21 | ```
22 | docker-compose down
23 | # or
24 | make down
25 |
26 | # delete all previous containers and volume included
27 | docker-compose down -v --remove-orphans
28 | # or
29 | make down-all
30 | ```
31 |
32 | ### Log Monitor
33 |
34 | Monitor log for all services (mongodb, backend/api, frontend/vuejs)
35 | ```
36 | docker-compose logs -f
37 | # or
38 | make logs
39 | ```
40 |
41 | Monitor log for backend service only
42 | ```
43 | docker-compose logs -f backend
44 | # or
45 | make logs-backend
46 | ```
47 |
48 | ### Run Test for the backend service
49 | ```
50 | docker-compose exec backend pytest
51 | # or
52 | make test-backend
53 | ```
54 |
55 | ----------------------------
56 |
57 | The API Documentation url:
58 | http://127.0.0.1:8000/docs
59 |
60 |
61 | The Blog:
62 | http://127.0.0.1:8080
63 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | backend:
5 | build: .
6 | command: uvicorn src.main:app --reload --host 0.0.0.0 --port 8000
7 | environment:
8 | BASE_PATH_API: /api
9 | SECRET_KEY: secretkey
10 | ALLOWED_HOSTS: 127.0.0.1,localhost
11 | MONGODB_NAME: blog-fastapi
12 | MONGODB_URI: mongodb://mongodbuser:mongodbpassword@mongodb:27017
13 | volumes:
14 | - ./src/:/home/app/backend/src
15 | env_file:
16 | - ./.env
17 | depends_on:
18 | - mongodb
19 | ports:
20 | - "8000:8000"
21 | frontend:
22 | build: ./frontend
23 | volumes:
24 | - ./frontend:/home/app/frontend
25 | - /home/app/frontend/node_modules
26 | depends_on:
27 | - backend
28 | ports:
29 | - "8080:8080"
30 | mongodb:
31 | image: mongo:4.0.8
32 | restart: unless-stopped
33 | command: mongod --auth
34 | environment:
35 | MONGO_INITDB_ROOT_USERNAME: mongodbuser
36 | MONGO_INITDB_ROOT_PASSWORD: mongodbpassword
37 | MONGO_INITDB_DATABASE: blog-fastapi
38 | MONGODB_DATA_DIR: /data/db
39 | MONDODB_LOG_DIR: /dev/null
40 | volumes:
41 | - mongodbdata:/data/db
42 |
43 |
44 | volumes:
45 | mongodbdata:
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .dockerignore
3 | .gitignore
4 | .github/*
5 | README.md
6 | Dockerfile
7 |
8 | # Artifacts that will be built during image creation.
9 | # This should contain all files created during `yarn build`.
10 | dist
11 | node_modules
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine
2 |
3 | WORKDIR /home/app/frontend
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install
8 |
9 | EXPOSE 8080
10 |
11 | CMD ["npm", "run", "serve"]
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # frontend
2 |
3 | ## Project setup
4 | ```
5 | yarn install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | yarn run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | yarn run build
16 | ```
17 |
18 | ### Run your tests
19 | ```
20 | yarn run test
21 | ```
22 |
23 | ### Lints and fixes files
24 | ```
25 | yarn run lint
26 | ```
27 |
28 | ### Customize configuration
29 | See [Configuration Reference](https://cli.vuejs.org/config/).
30 |
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "axios": "^0.21.2",
12 | "bootstrap": "^4.4.1",
13 | "core-js": "^3.6.4",
14 | "nprogress": "^0.2.0",
15 | "vue": "^2.6.11",
16 | "vue-axios": "^2.1.5",
17 | "vue-router": "^3.1.6"
18 | },
19 | "devDependencies": {
20 | "@vue/cli-plugin-babel": "^4.2.0",
21 | "@vue/cli-plugin-eslint": "^4.2.0",
22 | "@vue/cli-service": "^4.2.0",
23 | "babel-eslint": "^10.0.3",
24 | "eslint": "^6.7.2",
25 | "eslint-plugin-vue": "^6.1.2",
26 | "vue-template-compiler": "^2.6.11"
27 | },
28 | "eslintConfig": {
29 | "root": true,
30 | "env": {
31 | "node": true
32 | },
33 | "extends": [
34 | "plugin:vue/essential",
35 | "eslint:recommended"
36 | ],
37 | "parserOptions": {
38 | "parser": "babel-eslint"
39 | },
40 | "rules": {}
41 | },
42 | "browserslist": [
43 | "> 1%",
44 | "last 2 versions"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwisulfahnur/blog-fastapi-vuejs/66c05d5bc0e78df98a1e294568a9f0af16a0e704/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Blog
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwisulfahnur/blog-fastapi-vuejs/66c05d5bc0e78df98a1e294568a9f0af16a0e704/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
8 |
9 |
Installed CLI Plugins
10 |
14 |
Essential Links
15 |
22 |
Ecosystem
23 |
30 |
31 |
32 |
33 |
41 |
42 |
43 |
59 |
--------------------------------------------------------------------------------
/frontend/src/components/Nav.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
41 |
63 |
64 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 | Invalid Email or Password
11 |
12 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
74 |
75 |
--------------------------------------------------------------------------------
/frontend/src/components/posts/EditPost.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
37 |
38 |
39 |
Not Found
40 |
41 |
42 |
43 |
44 |
45 |
46 |
96 |
97 |
--------------------------------------------------------------------------------
/frontend/src/components/posts/MyPosts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Loading...
7 |
8 |
9 |
10 |
11 | My Posts
12 |
13 |
14 |
15 |
16 | # |
17 | Title |
18 | Actions |
19 |
20 |
21 |
22 |
23 |
24 | {{ index + 1 }} |
25 | {{ post.title }} |
26 |
27 | Edit
29 |
30 | Delete
31 | |
32 |
33 |
34 |
35 |
36 |
37 |
You don't have any post
38 | Create a new one
39 |
40 |
41 |
42 |
43 |
44 |
74 |
75 |
--------------------------------------------------------------------------------
/frontend/src/components/posts/NewPost.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
74 |
75 |
--------------------------------------------------------------------------------
/frontend/src/components/posts/PublicPosts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{post.title}}
8 |
9 |
Author: {{ post.author_name }}
10 |
Author: Unknown
11 |
12 |
{{post.content}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
34 |
35 |
--------------------------------------------------------------------------------
/frontend/src/components/users/EditUser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Edit User
4 |
5 |
6 |
--------------------------------------------------------------------------------
/frontend/src/components/users/ListUser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
User List
4 |
5 |
6 |
7 |
8 | ID |
9 | Full Name |
10 | Email |
11 | Actions |
12 |
13 |
14 |
15 |
16 |
17 | {{ user.id }} |
18 | {{ user.first_name }} {{ user.last_name }} |
19 | {{ user.email }} |
20 |
21 | Edit
25 |
26 |
27 |
28 | |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/src/components/users/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 | import VueAxios from 'vue-axios'
4 | import axios from 'axios'
5 | import NProgress from 'nprogress'
6 |
7 | import App from './App.vue'
8 | import CreateUser from './components/users/Register'
9 | import EditUser from './components/users/EditUser'
10 | import Login from './components/auth/Login'
11 | import 'bootstrap/dist/css/bootstrap.css'
12 | import MyPosts from "./components/posts/MyPosts";
13 | import PublicPosts from "./components/posts/PublicPosts";
14 | import NewPost from "./components/posts/NewPost";
15 | import EditPost from "./components/posts/EditPost";
16 | // import ListUser from './components/users/ListUser'
17 |
18 |
19 | Vue.use(VueRouter)
20 | Vue.use(VueAxios, axios.create({
21 | baseURL: 'http://localhost:8000/api',
22 | headers: {
23 | Authorization: `Bearer ${localStorage.getItem('actoken')}`
24 | }
25 | }))
26 |
27 | Vue.config.productionTip = false
28 |
29 |
30 | const routes = [
31 | // {
32 | // name: 'user-list',
33 | // path: '/users/',
34 | // component: ListUser
35 | // },
36 | {
37 | name: 'Home',
38 | path: '/',
39 | component: PublicPosts
40 | },
41 | {
42 | name: 'login',
43 | path: '/login',
44 | component: Login
45 | },
46 | {
47 | name: 'register',
48 | path: '/register',
49 | component: CreateUser
50 | },
51 | {
52 | name: 'user-edit',
53 | path: '/users/edit',
54 | component: EditUser
55 | },
56 | {
57 | name: 'new-post',
58 | path: '/post/new',
59 | component: NewPost
60 | },
61 | {
62 | name: 'my-posts',
63 | path: '/post/me',
64 | component: MyPosts,
65 | },
66 | {
67 | name: 'edit-post',
68 | path: '/post/:id',
69 | component: EditPost
70 | }
71 | ]
72 |
73 |
74 | const router = new VueRouter({mode: 'history', routes: routes})
75 |
76 | router.beforeResolve((to, from, next) => {
77 | if (to.name) {
78 | NProgress.start()
79 | }
80 | next()
81 | })
82 |
83 | router.afterEach(() => {
84 | NProgress.done()
85 | })
86 |
87 | new Vue({
88 | render: h => h(App),
89 | router
90 | }).$mount('#app')
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | up:
2 | docker-compose up -d --build
3 |
4 | down:
5 | docker-compose down
6 |
7 | down-all:
8 | docker-compose down -v --remove-orphans
9 |
10 | logs:
11 | docker-compose logs -f
12 |
13 | logs-backend:
14 | docker-compose logs -f backend
15 |
16 | test-backend:
17 | docker-compose exec backend pytest
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | attrs>=19.3.0
2 | bcrypt>=3.1.7
3 | certifi>=2019.11.28
4 | cffi>=1.14.0
5 | chardet>=3.0.4
6 | Click>=7.0
7 | coverage>=5.0.3
8 | dnspython>=1.16.0
9 | email-validator>=1.0.5
10 | fastapi>=0.49.0
11 | h11>=0.9.0
12 | httptools>=0.1.1
13 | idna>=2.9
14 | importlib-metadata>=1.5.0
15 | marshmallow>=2.20.5
16 | more-itertools>=8.2.0
17 | motor>=2.1.0
18 | packaging>=20.1
19 | passlib>=1.7.2
20 | pluggy>=0.13.1
21 | py>=1.8.1
22 | pycparser>=2.20
23 | pydantic>=1.4
24 | PyJWT>=1.7.1
25 | pymongo>=3.10.1
26 | pyparsing>=2.4.6
27 | pytest>=5.3.5
28 | python-dateutil>=2.8.1
29 | python-multipart>=0.0.5
30 | requests>=2.23.0
31 | six>=1.14.0
32 | starlette>=0.12.9
33 | tests>=0.7
34 | umongo>=3.0.0
35 | urllib3>=1.25.8
36 | uvicorn>=0.11.7
37 | uvloop>=0.14.0
38 | wcwidth>=0.1.8
39 | websockets>=8.1
40 | zipp>=3.0.0
41 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwisulfahnur/blog-fastapi-vuejs/66c05d5bc0e78df98a1e294568a9f0af16a0e704/src/__init__.py
--------------------------------------------------------------------------------
/src/api/__init__.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 |
3 | from .auth_api import router as oauth_router
4 | from .post_api import router as post_router
5 | from .user_api import router as user_router
6 |
7 | router = APIRouter()
8 |
9 | router.include_router(prefix="/oauth", router=oauth_router, tags=['OAuth2'])
10 | router.include_router(prefix="/users", router=user_router, tags=['Users'])
11 | router.include_router(prefix="/posts", router=post_router, tags=['Posts'])
12 |
--------------------------------------------------------------------------------
/src/api/auth_api.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | import jwt
4 | from fastapi import APIRouter, Depends, HTTPException
5 | from fastapi.security import OAuth2PasswordBearer
6 | from fastapi.security import OAuth2PasswordRequestForm
7 | from starlette import status
8 |
9 | from src.core.config import ACCESS_TOKEN_EXPIRES, SECRET_KEY, ALGORITHM
10 | from src.models.user import User
11 | from src.serializers.token import Token, TokenData
12 |
13 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/api/oauth/token')
14 | router = APIRouter()
15 |
16 |
17 | async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
18 | credentials_exception = HTTPException(
19 | status_code=status.HTTP_401_UNAUTHORIZED,
20 | detail="Could not validate credentials",
21 | headers={"WWW-Authenticate": "Bearer"},
22 | )
23 | try:
24 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
25 | sub: str = payload.get("sub")
26 | if sub is None:
27 | raise credentials_exception
28 | token_data = TokenData(sub=sub)
29 | except jwt.PyJWTError:
30 | raise credentials_exception
31 | user = await User.get(token_data.sub)
32 | if user is None:
33 | raise credentials_exception
34 | return user
35 |
36 |
37 | @router.post('/token', response_model=Token)
38 | async def get_token_api(form_data: OAuth2PasswordRequestForm = Depends()):
39 | user = await User.get_by_email(form_data.username)
40 | if not user:
41 | raise HTTPException(status_code=400, detail="Incorrect username or password")
42 |
43 | if not user.check_password(form_data.password):
44 | raise HTTPException(status_code=400, detail="Incorrect username or password")
45 |
46 | access_token = user.create_access_token(expires_delta=timedelta(hours=ACCESS_TOKEN_EXPIRES))
47 | return {"access_token": access_token, "token_type": "bearer"}
48 |
49 |
50 | @router.get('/tokeninfo', status_code=status.HTTP_200_OK)
51 | async def get_token_info_api(token: str = None):
52 | credentials_exception = HTTPException(
53 | detail='Token is not valid',
54 | status_code=status.HTTP_401_UNAUTHORIZED)
55 | if not token: raise credentials_exception
56 | try:
57 | payload = jwt.decode(token.encode(), SECRET_KEY, algorithms=[ALGORITHM])
58 | sub: str = payload.get("sub")
59 | if sub is None or not await User.get(sub):
60 | raise credentials_exception
61 | except jwt.PyJWTError:
62 | raise credentials_exception
63 | return payload
64 |
--------------------------------------------------------------------------------
/src/api/exceptions.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwisulfahnur/blog-fastapi-vuejs/66c05d5bc0e78df98a1e294568a9f0af16a0e704/src/api/exceptions.py
--------------------------------------------------------------------------------
/src/api/post_api.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import List
3 |
4 | from bson.objectid import ObjectId
5 | from fastapi import APIRouter, HTTPException, Depends
6 | from starlette import status
7 |
8 | from src.api.auth_api import get_current_user
9 | from src.models.post import Post, Comment
10 | from src.models.user import User
11 | from src.serializers.comment import CommentInSerializer
12 | from src.serializers.post import PostInSerializer, PostSerializer, PostInPatchSerializer
13 |
14 | router = APIRouter()
15 |
16 |
17 | def pagination(skip: int = 0, limit: int = 10):
18 | return {
19 | 'skip': skip,
20 | 'limit': limit
21 | }
22 |
23 |
24 | @router.get("", response_model=List[PostSerializer],
25 | status_code=status.HTTP_200_OK)
26 | async def get_posts_api(pagination=Depends(pagination)):
27 | cursor = Post.find() \
28 | .sort('created_at', -1) \
29 | .skip(pagination['skip']) \
30 | .limit(pagination['limit'])
31 |
32 | posts = [post.dump() for post in await cursor.to_list(length=pagination['limit'])]
33 | return posts
34 |
35 |
36 | @router.get("/me", response_model=List[PostSerializer])
37 | async def get_my_posts_api(pagination=Depends(pagination), current_user=Depends(get_current_user)):
38 | cursor = Post.find({'created_by': current_user.id}) \
39 | .sort('created_at', -1) \
40 | .skip(pagination['skip']) \
41 | .limit(pagination['limit'])
42 |
43 | posts = [post.dump() for post in await cursor.to_list(length=pagination['limit'])]
44 | return posts
45 |
46 |
47 | @router.post("", response_model=PostSerializer,
48 | status_code=status.HTTP_201_CREATED)
49 | async def create_post_api(post: PostInSerializer, current_user:User=Depends(get_current_user)):
50 | post = Post(
51 | title=post.title,
52 | created_by=current_user.id,
53 | content=post.content,
54 | comments=[],
55 | author_name=current_user.full_name
56 | )
57 |
58 | await post.commit()
59 | post = post.dump()
60 | return post
61 |
62 |
63 | @router.get("/{post_id}", response_model=PostSerializer,
64 | status_code=status.HTTP_201_CREATED)
65 | async def get_post_detail_api(post_id: str):
66 | post = await Post.find_one({"_id": ObjectId(post_id)})
67 | if post:
68 | return post.dump()
69 | else:
70 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Post Not Found")
71 |
72 |
73 | @router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
74 | async def delete_post_api(post_id: str, current_user=Depends(get_current_user)):
75 | post = await Post.get(post_id)
76 | if not post:
77 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Post Not Found")
78 |
79 | if post.created_by != current_user:
80 | raise HTTPException(status_code=401, detail='Not Enough Permissions')
81 |
82 | await post.remove()
83 | return
84 |
85 |
86 | @router.patch("/{post_id}", response_model=PostSerializer, status_code=status.HTTP_200_OK)
87 | async def update_post_api(post_id: str, post_in: PostInPatchSerializer, current_user=Depends(get_current_user)):
88 | post = await Post.get(post_id)
89 | if not post:
90 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Post Not Found")
91 |
92 | if post.created_by != current_user:
93 | raise HTTPException(status_code=401, detail='Not Enough Permissions')
94 |
95 | if post_in.title:
96 | post.title = post_in.title
97 | if post_in.content:
98 | post.content = post_in.content
99 |
100 | post.updated_at = datetime.now()
101 | await post.commit()
102 | return post.dump()
103 |
104 |
105 | @router.post("/{post_id}/add-comment", response_model=PostSerializer,
106 | status_code=status.HTTP_201_CREATED)
107 | async def add_post_comment_api(post_id: str, comment: CommentInSerializer,
108 | current_user: User = Depends(get_current_user)):
109 | post = await Post.find_one({"_id": ObjectId(post_id)})
110 |
111 | if post:
112 | comment = Comment(created_by=current_user, content=comment.content)
113 | post.add_comment(comment)
114 | await post.commit()
115 | return post.dump()
116 |
117 | if not post:
118 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
119 | detail="Post Not Found")
120 |
--------------------------------------------------------------------------------
/src/api/user_api.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from bson.objectid import ObjectId
4 | from fastapi import APIRouter, HTTPException, Depends
5 | from marshmallow.exceptions import ValidationError
6 | from starlette import status
7 | from starlette.responses import JSONResponse
8 |
9 | from src.models.user import User
10 | from src.serializers.user import UserInSerializer, UserSerializer
11 | from .auth_api import get_current_user
12 |
13 | router = APIRouter()
14 |
15 |
16 | @router.get("", response_model=List[UserSerializer],
17 | status_code=status.HTTP_200_OK)
18 | async def get_users_api(_: User = Depends(get_current_user)):
19 | cursor = User.find()
20 | return [user.dump() for user in await cursor.to_list(length=10)]
21 |
22 |
23 | @router.post("", response_model=UserSerializer,
24 | status_code=status.HTTP_201_CREATED)
25 | async def create_user_api(user_in: UserInSerializer):
26 | user_exists = await User.get_by_email(user_in.email)
27 | if user_exists:
28 | raise HTTPException(
29 | 422,
30 | [{
31 | 'loc': ["body", 'email'],
32 | 'msg': f"Email '{user_in.email}' is already registered",
33 | 'type': 'value_error',
34 | }]
35 | )
36 | try:
37 | user = await User.register_new_user(email=user_in.email,
38 | full_name=user_in.full_name,
39 | password=user_in.password)
40 | return user.dump()
41 | except ValidationError as e:
42 | return JSONResponse({'field_errors': [e.messages]}, status.HTTP_400_BAD_REQUEST)
43 |
44 |
45 | @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
46 | async def delete_user_api(user_id: str, current_user: User = Depends(get_current_user)):
47 | user = await User.find_one({'_id': ObjectId(user_id)}, {'_id': 1})
48 | if not user:
49 | raise HTTPException(detail="User Not Found", status_code=status.HTTP_404_NOT_FOUND)
50 | await user.remove()
51 |
52 |
53 | @router.get('/me', response_model=UserSerializer, status_code=status.HTTP_200_OK)
54 | async def get_user_me_api(current_user: User = Depends(get_current_user)):
55 | return current_user.dump()
56 |
--------------------------------------------------------------------------------
/src/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwisulfahnur/blog-fastapi-vuejs/66c05d5bc0e78df98a1e294568a9f0af16a0e704/src/core/__init__.py
--------------------------------------------------------------------------------
/src/core/config.py:
--------------------------------------------------------------------------------
1 | from passlib.context import CryptContext
2 | from starlette.config import Config, environ
3 | from starlette.datastructures import CommaSeparatedStrings
4 |
5 | config = Config(".env")
6 |
7 | BASE_PATH_API = config('BASE_PATH_API')
8 |
9 | SECRET_KEY = config('SECRET_KEY', default='secretkey')
10 | ALGORITHM = "HS256"
11 | PWD_CONTEXT = CryptContext(schemes=["bcrypt"], deprecated="auto")
12 |
13 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=CommaSeparatedStrings, default=['*'])
14 |
15 | DEBUG = config('DEBUG', cast=bool, default=False)
16 |
17 | ACCESS_TOKEN_EXPIRES = config('ACCESS_TOKEN_EXPIRES', default=24 * 7)
18 | MONGODB_NAME = config("MONGODB_NAME")
19 | MONGODB_URI = config("MONGODB_URI")
20 |
21 | database_name = MONGODB_NAME
22 | if environ.get('TESTING') == 'TRUE':
23 | database_name = f'test-{database_name}'
24 |
--------------------------------------------------------------------------------
/src/core/db.py:
--------------------------------------------------------------------------------
1 | from src.core import config
2 | from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
3 | from umongo.frameworks import MotorAsyncIOInstance
4 |
5 |
6 | class DataBase:
7 | client: AsyncIOMotorClient = None
8 |
9 |
10 | database = DataBase()
11 |
12 |
13 | def get_database() -> AsyncIOMotorDatabase:
14 | if database.client:
15 | return database.client[config.database_name]
16 | return AsyncIOMotorClient(config.MONGODB_URI)[config.database_name]
17 |
18 |
19 | def get_client() -> AsyncIOMotorClient:
20 | if not database.client:
21 | return AsyncIOMotorClient(config.MONGODB_URI)
22 | return database.client
23 |
24 |
25 | db = get_database()
26 | instance = MotorAsyncIOInstance(db)
27 |
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from fastapi import FastAPI
4 | from starlette.middleware.cors import CORSMiddleware
5 | from motor.motor_asyncio import AsyncIOMotorClient
6 |
7 | from src.api import router
8 | from src.core import config
9 | from src.core.db import database
10 |
11 | app = FastAPI(title="Blog API")
12 |
13 | origins= [
14 | 'http://localhost:8000',
15 | 'http://localhost:8080',
16 | 'http://127.0.0.1:8000',
17 | 'http://127.0.0.1:8080'
18 | ]
19 |
20 | app.add_middleware(
21 | CORSMiddleware,
22 | allow_origins=origins,
23 | allow_credentials=True,
24 | allow_methods=['*'],
25 | allow_headers=['*'],
26 | )
27 |
28 |
29 | @app.on_event("startup")
30 | async def event_startup():
31 | from src.core.db import database
32 | logging.info("connect to database....")
33 | database.client = AsyncIOMotorClient(str(config.MONGODB_URI))
34 | logging.info("Connected to database!")
35 |
36 | logging.info("ensuring model indexes")
37 | from src.models import ensure_indexes
38 | await ensure_indexes()
39 |
40 |
41 | @app.on_event("shutdown")
42 | async def event_shutdown():
43 | logging.info("close mongodb connection....")
44 | database.client.close()
45 | logging.info("connection to mongodb has been closed!")
46 |
47 |
48 | app.include_router(router, prefix=config.BASE_PATH_API)
49 |
--------------------------------------------------------------------------------
/src/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .post import Post
2 | from .user import User
3 |
4 |
5 | async def ensure_indexes():
6 | await User.ensure_indexes()
7 | await Post.ensure_indexes()
8 |
--------------------------------------------------------------------------------
/src/models/post.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional
3 |
4 | from bson.objectid import ObjectId
5 | from umongo import Document, fields, EmbeddedDocument
6 |
7 | from src.core.db import instance, db
8 | from src.models.user import User
9 |
10 |
11 | @instance.register
12 | class Comment(EmbeddedDocument):
13 | id = fields.ObjectIdField(default=ObjectId())
14 | content = fields.StrField()
15 |
16 | created_by = fields.ReferenceField("User")
17 | created_at = fields.DateTimeField(default=datetime.now)
18 | updated_at = fields.DateTimeField(default=datetime.now)
19 |
20 |
21 | @instance.register
22 | class Post(Document):
23 | title = fields.StrField()
24 | content = fields.StrField()
25 | comments = fields.ListField(fields.EmbeddedField(Comment))
26 |
27 | author_name = fields.StrField(allow_none=True, default='')
28 | created_by = fields.ReferenceField(User)
29 | created_at = fields.DateTimeField(default=datetime.now)
30 | updated_at = fields.DateTimeField(default=datetime.now)
31 |
32 | class Meta:
33 | collection = db.post
34 |
35 | @classmethod
36 | async def get(cls, id: str) -> Optional['Post']:
37 | if not ObjectId.is_valid(id):
38 | return None
39 |
40 | return await cls.find_one({'_id': ObjectId(id)})
41 |
42 | def add_comment(self, comment: Comment):
43 | self.comments = self.comments + [comment]
44 |
--------------------------------------------------------------------------------
/src/models/user.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 | import jwt
4 | from bson.objectid import ObjectId
5 | from src.core.config import SECRET_KEY, PWD_CONTEXT, ALGORITHM
6 | from src.core.db import instance, db
7 | from umongo import Document, fields
8 |
9 |
10 | @instance.register
11 | class User(Document):
12 | id = fields.ObjectIdField()
13 | full_name = fields.StrField()
14 | email = fields.EmailField(unique=True)
15 | hashed_password = fields.StrField()
16 | last_password_updated_at = fields.DateTimeField()
17 | scopes = fields.ListField(fields.StrField(), default=[])
18 |
19 | created_at = fields.DateTimeField(default=datetime.now)
20 | updated_at = fields.DateTimeField(default=datetime.now)
21 |
22 | class Meta:
23 | collection_name = 'user'
24 | collection = db.user
25 |
26 | @classmethod
27 | async def get(cls, id: str):
28 | if not ObjectId.is_valid(id):
29 | return None
30 | user = await cls.find_one({'_id': ObjectId(id)})
31 | return user
32 |
33 | @classmethod
34 | async def get_by_email(cls, email: str):
35 | return await cls.find_one({'email': email})
36 |
37 | def check_password(self, password: str):
38 | if self.hashed_password:
39 | return PWD_CONTEXT.verify(password, self.hashed_password)
40 |
41 | def set_password(self, password: str):
42 | self.hashed_password = PWD_CONTEXT.hash(password)
43 | self.last_password_updated_at = datetime.now()
44 |
45 | def create_access_token(self, expires_delta: timedelta = None):
46 | now = datetime.utcnow()
47 | if expires_delta:
48 | expire = now + expires_delta
49 | else:
50 | expire = now + timedelta(minutes=15)
51 | to_encode = {
52 | 'exp': expire,
53 | 'iat': now,
54 | 'sub': str(self.id),
55 | 'scope': ' '.join(self.scopes) if self.scopes else ''
56 | }
57 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
58 | return encoded_jwt
59 |
60 | @classmethod
61 | async def register_new_user(cls, email: str, full_name: str, password: str):
62 | user = cls(email=email, full_name=full_name)
63 | user.set_password(password)
64 | await user.commit()
65 | return user
66 |
--------------------------------------------------------------------------------
/src/serializers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwisulfahnur/blog-fastapi-vuejs/66c05d5bc0e78df98a1e294568a9f0af16a0e704/src/serializers/__init__.py
--------------------------------------------------------------------------------
/src/serializers/comment.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class CommentSerializer(BaseModel):
7 | id: str
8 | content: str
9 |
10 | created_by: str
11 | created_at: datetime
12 | updated_at: datetime
13 |
14 |
15 | class CommentInSerializer(BaseModel):
16 | content: str
17 |
--------------------------------------------------------------------------------
/src/serializers/post.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import List, Optional
3 |
4 | from pydantic import BaseModel, validator
5 |
6 | from .comment import CommentSerializer
7 |
8 |
9 | class PostSerializer(BaseModel):
10 | id: str
11 | title: str
12 | content: str
13 | comments: List[CommentSerializer] = []
14 |
15 | author_name: Optional[str]
16 | created_by: str
17 | created_at: datetime
18 | updated_at: datetime
19 |
20 |
21 | class PostInSerializer(BaseModel):
22 | title: str
23 | content: str
24 |
25 | @validator('title')
26 | def validate_title(cls, v):
27 | if not v:
28 | raise ValueError('This field is required')
29 | if len(v) < 5:
30 | raise ValueError('Minimum length of title is 5 chars')
31 | return v
32 |
33 | @validator('content')
34 | def validate_content(cls, v):
35 | if not v:
36 | raise ValueError('This field is required')
37 | if len(v) < 20:
38 | raise ValueError('Minimum length of the body of content is 20 chars')
39 | return v
40 |
41 |
42 | class PostInPatchSerializer(BaseModel):
43 | title: Optional[str]
44 | content: Optional[str]
45 |
--------------------------------------------------------------------------------
/src/serializers/token.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class Token(BaseModel):
7 | access_token: str
8 | token_type: str
9 |
10 |
11 | class TokenData(BaseModel):
12 | sub: str = None
13 | scopes: List[str] = []
14 |
--------------------------------------------------------------------------------
/src/serializers/user.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional
3 |
4 | from pydantic import BaseModel, EmailStr, validator
5 |
6 |
7 | class UserSerializer(BaseModel):
8 | id: str
9 | full_name: Optional[str]
10 | email: EmailStr
11 |
12 | created_at: datetime
13 | updated_at: datetime
14 |
15 | class Config:
16 | orm_mode = True
17 |
18 |
19 | class UserInSerializer(BaseModel):
20 | full_name: str
21 | email: EmailStr
22 | password: str
23 |
24 | @validator('full_name')
25 | def validate_full_name(cls, v):
26 | if not v:
27 | raise ValueError('Full Name is required')
28 | return v
29 |
30 | @validator('password')
31 | def validate_password(cls, v):
32 | if not v:
33 | raise ValueError('Password is required to create a new account')
34 | if v and len(v) < 8:
35 | raise ValueError('Password must be more than 8 characters')
36 | return v
37 |
--------------------------------------------------------------------------------
/src/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwisulfahnur/blog-fastapi-vuejs/66c05d5bc0e78df98a1e294568a9f0af16a0e704/src/tests/__init__.py
--------------------------------------------------------------------------------
/src/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from starlette.config import environ
3 | from starlette.testclient import TestClient
4 | from datetime import timedelta
5 | environ["TESTING"] = "TRUE"
6 |
7 |
8 | @pytest.fixture(scope="session")
9 | def test_user_data():
10 | return {
11 | "email": "user1@example.com",
12 | "full_name": "string test1",
13 | "password": "string1-min8",
14 | }
15 |
16 |
17 | @pytest.fixture(scope='session')
18 | def event_loop():
19 | import asyncio
20 | loop = asyncio.get_event_loop()
21 | return loop
22 |
23 |
24 | @pytest.fixture(scope="session")
25 | def test_user_object_data(event_loop, test_user_data):
26 | from src.models.user import User
27 | user = event_loop.run_until_complete(User.get_by_email(test_user_data['email']))
28 | if not user:
29 | user = event_loop.run_until_complete(
30 | User.register_new_user(
31 | email=test_user_data['email'],
32 | full_name=test_user_data['full_name'],
33 | password=test_user_data['password'],
34 | ))
35 | return user
36 |
37 |
38 | @pytest.fixture(scope="session")
39 | def access_token(test_user_object_data):
40 | return test_user_object_data.create_access_token(timedelta(hours=1))
41 |
42 |
43 | @pytest.fixture(scope="session")
44 | def test_client(event_loop):
45 | from src.main import app
46 | from src.core.config import database_name
47 | with TestClient(app) as test_client:
48 | yield test_client
49 | app.dependency_overrides = {}
50 |
51 | # Delete database testing after test already finished
52 | from src.core.db import get_client
53 | client = get_client()
54 | client.drop_database(database_name)
55 |
--------------------------------------------------------------------------------
/src/tests/endpoints/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwisulfahnur/blog-fastapi-vuejs/66c05d5bc0e78df98a1e294568a9f0af16a0e704/src/tests/endpoints/__init__.py
--------------------------------------------------------------------------------
/src/tests/endpoints/test_oauth.py:
--------------------------------------------------------------------------------
1 | from src.core.config import BASE_PATH_API
2 |
3 |
4 | def test_obtain_token_api(test_client, test_user_data, test_user_object_data):
5 | response = test_client.post(f'{BASE_PATH_API}/oauth/token', data={
6 | 'grant_type': 'password',
7 | 'username': test_user_data['email'],
8 | 'password': test_user_data['password'],
9 | })
10 |
11 | assert response.status_code == 200
12 | assert response.json()['access_token']
13 | assert response.json()['token_type'] == 'bearer'
14 |
15 |
16 | def test_obtain_token_info_api(test_client, access_token):
17 | response = test_client.get(f'{BASE_PATH_API}/oauth/tokeninfo?token={access_token}')
18 | assert response.status_code == 200
19 |
20 |
21 | def test_obtain_token_info_api_failed(test_client):
22 | response = test_client.get(f'{BASE_PATH_API}/oauth/tokeninfo?token=wrongtoken')
23 | assert response.status_code == 401
24 | assert response.json()['detail'] == 'Token is not valid'
--------------------------------------------------------------------------------
/src/tests/endpoints/test_post.py:
--------------------------------------------------------------------------------
1 |
2 | import pytest
3 |
4 | from src.core.config import BASE_PATH_API
5 |
6 |
7 | @pytest.fixture(scope='module')
8 | def test_post_data():
9 | return {
10 | 'title': 'Test Post title',
11 | 'content': 'Test content of the post'
12 | }
13 |
14 |
15 | @pytest.fixture(scope='module')
16 | def test_create_post(test_client, test_user_object_data, test_post_data, access_token):
17 | response = test_client.post(f'{BASE_PATH_API}/posts', json=test_post_data, headers={
18 | 'Authorization': f'Bearer {access_token}'
19 | })
20 |
21 | assert response.status_code == 201
22 | assert response.json()['title'] == test_post_data['title']
23 | assert response.json()['content'] == test_post_data['content']
24 |
25 | def test_create_post_fail(test_client, access_token):
26 | response = test_client.post(f'{BASE_PATH_API}/posts', headers={
27 | 'Authorization': f'Bearer {access_token}'
28 | })
29 |
30 | assert response.status_code == 422
31 | assert response.json()
32 |
33 | def test_create_post_unauthorized(test_client, test_post_data):
34 | response = test_client.post(f'{BASE_PATH_API}/posts', json=test_post_data, headers={
35 | 'Authorization': f'Bearer nope'
36 | })
37 |
38 | assert response.status_code == 401
39 | assert response.json()
40 |
41 | def test_read_post(test_client, test_create_post):
42 | response = test_client.get(f'{BASE_PATH_API}/posts')
43 |
44 | assert response.status_code == 200
45 | assert response.json()
46 |
--------------------------------------------------------------------------------
/src/tests/endpoints/test_user.py:
--------------------------------------------------------------------------------
1 | from src.core.config import BASE_PATH_API
2 |
3 |
4 | def test_user_register_api(test_client, test_user_data, test_user_object_data, monkeypatch):
5 | from src.models.user import User
6 | class UserInDBMock(User):
7 | @classmethod
8 | async def register_new_user(cls, full_name: str,
9 | email: str, password: str):
10 | return test_user_object_data
11 |
12 | monkeypatch.setattr('src.models.user.User.register_new_user',
13 | UserInDBMock.register_new_user)
14 |
15 | response = test_client.post(f'{BASE_PATH_API}/users', json={
16 | 'full_name': test_user_data['full_name'],
17 | 'email': test_user_data['email'],
18 | 'password': test_user_data['password'],
19 | })
20 |
21 | assert response.status_code == 201
22 | assert response.json()['id']
23 | assert response.json()['email'] == test_user_data['email']
24 |
25 |
26 | def test_read_user_me(test_client, test_user_object_data, access_token):
27 | response = test_client.get(f'{BASE_PATH_API}/users/me', headers={
28 | 'Authorization': f'Bearer {access_token}'
29 | })
30 |
31 | assert response.status_code == 200
32 | assert response.json()['id'] == str(test_user_object_data.id)
33 | assert response.json()['full_name'] == test_user_object_data.full_name
34 | assert response.json()['email'] == test_user_object_data.email
35 |
--------------------------------------------------------------------------------