next.js frontend w/ mongodb local backend

This commit is contained in:
isekaijoucyo 2025-02-07 10:31:32 +08:00
parent e24e17e42b
commit f333d9ba00
18 changed files with 3144 additions and 337 deletions

18
.gitignore vendored
View file

@ -131,5 +131,23 @@ dmypy.json
# OSX
.DS_Store
# Node.js / Next.js
node_modules/
.next/
.swc/
out/
build/
.vercel/
*.tsbuildinfo
# Node.js Debug Log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Local env files
.env*.local
# Etc
.idea

157
DEVELOPMENT.md Normal file
View file

@ -0,0 +1,157 @@
# 开发指南
## 环境要求
- Python 3.x
- Node.js (推荐 v16 或更高版本)
- MongoDB
## 数据库配置
1. 安装 MongoDB
- Windows: 参考 https://www.runoob.com/mongodb/mongodb-window-install.html
- macOS: 使用 Homebrew 安装
```bash
brew tap mongodb/brew
brew install mongodb-community
```
2. 启动 MongoDB 服务
```bash
# Windows
mongod --dbpath <你的数据库路径>
# macOS
brew services start mongodb-community
```
3. 验证 MongoDB 是否正常运行
```bash
mongosh
use flask_db
show dbs
```
## 后端服务启动
1. 安装 Python 依赖
```bash
# 如果使用 poetry
poetry install
# 如果使用 pip
pip install -r requirements.txt
```
2. 启动 Flask 服务器
```bash
# 使用 poetry
poetry run python -m flask --app src.backend.app run --debug
# 直接使用 python
python -m flask --app src.backend.app run --debug
# 如果localhost:5000相应api无法访问但是127.0.0.1:5000可以访问考虑使用以下命令启动
poetry run python -m flask --app src.backend.app run --host=0.0.0.0 --debug
# 在macos系统下可能要解决端口冲突
Port 5000 is in use by another program. Either identify and stop that program, or start the server with a different port.
On macOS, try disabling the 'AirPlay Receiver' service from System Preferences -> General -> AirDrop & Handoff.
```
服务器将在 http://localhost:5000 上运行
## 前端服务启动
1. 进入前端目录
```bash
cd src/frontend
```
2. 安装依赖
```bash
npm install
```
3. 启动开发服务器
```bash
npm run dev
```
前端服务将在 http://localhost:3000 上运行
## 常见问题
### MongoDB 连接问题
如果遇到 MongoDB 连接错误:
1. 确保 MongoDB 服务正在运行
2. 检查 MongoDB 默认端口 (27017) 是否被占用
3. 确认数据库路径是否正确
### 后端服务启动失败
1. 确保所有依赖都已正确安装
2. 检查 MongoDB 连接是否正常
3. 确保端口 5000 未被占用
### 前端开发服务器问题
1. 确保 Node.js 版本兼容
2. 删除 node_modules 目录并重新安装依赖
3. 确保端口 3000 未被占用
### 运行测试
1. 安装测试依赖
```bash
# 使用 poetry 添加测试依赖
poetry add pytest --dev
# 如果使用 pip
pip install pytest
```
2. 运行测试
使用 Poetry 运行测试:
```bash
# 运行所有测试
poetry run pytest
# 运行单个测试文件
poetry run pytest tests/test_app.py
# 查看详细的测试输出
poetry run pytest -v
# 显示测试失败的详细信息
poetry run pytest -vv
```
如果测试失败,使用 `-vv` 参数可以查看更详细的错误信息和断言失败的具体原因。
```
```bash
npm run dev
```
前端服务将在 http://localhost:3000 上运行
## 常见问题
### MongoDB 连接问题
如果遇到 MongoDB 连接错误:
1. 确保 MongoDB 服务正在运行
2. 检查 MongoDB 默认端口 (27017) 是否被占用
3. 确认数据库路径是否正确
### 后端服务启动失败
1. 确保所有依赖都已正确安装
2. 检查 MongoDB 连接是否正常
3. 确保端口 5000 未被占用
### 前端开发服务器问题
1. 确保 Node.js 版本兼容
2. 删除 node_modules 目录并重新安装依赖
3. 确保端口 3000 未被占用

View file

@ -0,0 +1,17 @@
import os
from datetime import datetime
# Define the database name and backup directory
db_name = "flask_db"
# Example: export BACKUP_DIR="/Users/<myname>/Codes/pyxr_db_backup"
backup_dir = os.getenv('BACKUP_DIR', '/default/backup/path') # Use environment variable
# Create a timestamped backup file name
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_file = os.path.join(backup_dir, f"{db_name}_backup_{timestamp}.gz")
# Run the mongodump command to back up the database
os.system(f"mongodump --db {db_name} --archive={backup_file} --gzip")
print(f"Backup completed: {backup_file}")

91
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "blinker"
@ -6,7 +6,6 @@ version = "1.9.0"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"},
{file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"},
@ -18,7 +17,6 @@ version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
@ -33,8 +31,6 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
markers = "platform_system == \"Windows\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@ -46,7 +42,6 @@ version = "2.7.0"
description = "DNS toolkit"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
{file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
@ -67,7 +62,6 @@ version = "3.1.0"
description = "A simple framework for building complex web applications."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"},
{file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"},
@ -84,13 +78,37 @@ Werkzeug = ">=3.1"
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
[[package]]
name = "flask-cors"
version = "4.0.2"
description = "A Flask extension adding a decorator for CORS support"
optional = false
python-versions = "*"
files = [
{file = "Flask_Cors-4.0.2-py2.py3-none-any.whl", hash = "sha256:38364faf1a7a5d0a55bd1d2e2f83ee9e359039182f5e6a029557e1f56d92c09a"},
{file = "flask_cors-4.0.2.tar.gz", hash = "sha256:493b98e2d1e2f1a4720a7af25693ef2fe32fbafec09a2f72c59f3e475eda61d2"},
]
[package.dependencies]
Flask = ">=0.9"
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
@ -102,7 +120,6 @@ version = "3.1.5"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
@ -120,7 +137,6 @@ version = "3.0.2"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
@ -185,13 +201,38 @@ files = [
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pymongo"
version = "4.11"
description = "Python driver for MongoDB <http://www.mongodb.org>"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pymongo-4.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1971039a8e3aab139e0382b26a9670cd34f43c5301da267360b9a640b637d09b"},
{file = "pymongo-4.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b4878e92ae255a05756399a4e2b428f0fd3529561eacd9f4781a70ad5311397e"},
@ -253,13 +294,32 @@ snappy = ["python-snappy"]
test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"]
zstd = ["zstandard"]
[[package]]
name = "pytest"
version = "8.3.4"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
groups = ["main"]
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"},
@ -274,7 +334,6 @@ version = "3.1.3"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
{file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
@ -287,6 +346,6 @@ MarkupSafe = ">=2.1.1"
watchdog = ["watchdog (>=2.3)"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.14"
content-hash = "92cefaa6220f3c4295418cd77943e3358937e37d8da7c2082d272a44801dad72"
lock-version = "2.0"
python-versions = ">=3.11,<4.0"
content-hash = "fb9237517aad08372c5658ca9803833ed0ddbc66f5b0778de7d03e315d6c3c35"

2
poetry.toml Normal file
View file

@ -0,0 +1,2 @@
[virtualenvs]
in-project = true

View file

@ -1,18 +1,20 @@
[project]
[tool.poetry]
name = "pyxr"
version = "0.1.0"
description = ""
authors = [
{name = "seemygesture",email = "seemygesture@gmail.com"}
]
authors = ["seemygesture <seemygesture@gmail.com>"]
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
"flask (>=3.1.0,<4.0.0)",
"pymongo (>=4.11,<5.0)",
"python-dotenv (>=1.0.1,<2.0.0)"
]
packages = [{include = "src"}]
[tool.poetry.dependencies]
python = ">=3.11,<4.0"
flask = ">=3.1.0,<4.0.0"
pymongo = ">=4.11,<5.0"
python-dotenv = ">=1.0.1,<2.0.0"
flask-cors = ">=4.0.0,<5.0.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.4"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]

View file

@ -1,26 +0,0 @@
from flask import Flask, render_template, request, url_for, redirect
from pymongo import MongoClient
from bson.objectid import ObjectId
app = Flask(__name__)
client = MongoClient('localhost', 27017)
db = client.flask_db
todos = db.todos
@app.route('/', methods=('GET', 'POST'))
def index():
if request.method=='POST':
content = request.form['content']
degree = request.form['degree']
todos.insert_one({'content': content, 'degree': degree})
return redirect(url_for('index'))
all_todos = todos.find()
return render_template('index.html', todos=all_todos)
@app.post('/<id>/delete/')
def delete(id):
todos.delete_one({"_id": ObjectId(id)})
return redirect(url_for('index'))

59
src/backend/app.py Normal file
View file

@ -0,0 +1,59 @@
from flask import Flask, request, jsonify
from flask_cors import CORS, cross_origin
from pymongo import MongoClient
from bson.objectid import ObjectId
from bson.json_util import dumps
from datetime import datetime
from bson import errors as bson_errors
import uuid
app = Flask(__name__)
cors = CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:3000", "http://localhost:3001"],
"methods": ["GET", "POST", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type"]
}
})
app.config['CORS_HEADERS'] = 'Content-Type'
client = MongoClient('localhost', 27017)
db = client.flask_db
todos = db.todos
# 确保UUID字段的唯一性
todos.create_index('uuid', unique=True)
@app.route('/api/todos', methods=['GET'])
@cross_origin()
def get_todos():
all_todos = todos.find()
return dumps(list(all_todos))
@app.route('/api/todos', methods=['POST'])
@cross_origin()
def create_todo():
content = request.json.get('content')
degree = request.json.get('degree')
timestamp = datetime.now()
todo_uuid = str(uuid.uuid4())
result = todos.insert_one({
'content': content,
'degree': degree,
'timestamp': timestamp,
'uuid': todo_uuid
})
return jsonify({'id': todo_uuid}), 201
@app.route('/api/todos/<uuid>', methods=['DELETE'])
@cross_origin()
def delete_todo(uuid):
result = todos.delete_one({"uuid": uuid})
if result.deleted_count > 0:
return '', 204
return jsonify({'error': 'Todo not found'}), 404
if __name__ == '__main__':
app.run(host='127.0.0.1', debug=True)

5
src/frontend/next-env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

2205
src/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
src/frontend/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "pyxr-frontend",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:5000",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.6.2"
},
"devDependencies": {
"@types/node": "^20.10.4",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"typescript": "^5.3.3",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6"
}
}

View file

@ -0,0 +1,16 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View file

@ -0,0 +1,128 @@
'use client';
import { useState, useEffect } from 'react';
import axios from 'axios';
interface Todo {
uuid: string;
content: string;
degree: 'Important' | 'Unimportant';
}
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([]);
const [content, setContent] = useState('');
const [degree, setDegree] = useState<'Important' | 'Unimportant'>('Important');
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
const response = await axios.get('http://localhost:5000/api/todos');
setTodos(response.data);
} catch (error) {
console.error('Error fetching todos:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await axios.post('http://localhost:5000/api/todos', { content, degree });
setContent('');
setDegree('Important');
fetchTodos();
} catch (error) {
console.error('Error creating todo:', error);
}
};
const handleDelete = async (uuid: string) => {
if (!confirm('Are you sure you want to delete this entry?')) return;
try {
await axios.delete(`http://localhost:5000/api/todos/${uuid}`);
fetchTodos();
} catch (error) {
console.error('Error deleting todo:', error);
}
};
return (
<main className="max-w-4xl mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">FlaskTODO</h1>
<hr className="mb-6" />
<form onSubmit={handleSubmit} className="mb-8 space-y-4">
<div>
<label htmlFor="content" className="block font-bold mb-2">
Todo content
</label>
<input
type="text"
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Todo Content"
className="w-full p-2 border rounded"
required
/>
</div>
<div className="space-y-2">
<p className="font-bold">Degree</p>
<div className="space-x-4">
<label className="inline-flex items-center">
<input
type="radio"
name="degree"
value="Important"
checked={degree === 'Important'}
onChange={(e) => setDegree(e.target.value as 'Important')}
className="mr-2"
/>
Important
</label>
<label className="inline-flex items-center">
<input
type="radio"
name="degree"
value="Unimportant"
checked={degree === 'Unimportant'}
onChange={(e) => setDegree(e.target.value as 'Unimportant')}
className="mr-2"
/>
Unimportant
</label>
</div>
</div>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Submit
</button>
</form>
<hr className="mb-6" />
<div className="space-y-4">
{todos.map((todo) => (
<div key={todo.uuid} className="bg-gray-100 p-4 rounded">
<p>
[{todo.uuid}]: {todo.content} <i>({todo.degree})</i>
</p>
<button
onClick={() => handleDelete(todo.uuid)}
className="mt-2 bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600"
>
Delete Todo
</button>
</div>
))}
</div>
</main>
);
}

View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

69
tests/test_app.py Normal file
View file

@ -0,0 +1,69 @@
import pytest
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure # Import ConnectionFailure
from datetime import datetime
from bson.objectid import ObjectId
from pyxr.app import app
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client
@pytest.fixture
def mongo_client():
# 使用测试数据库
client = MongoClient('localhost', 27017)
db = client.test_flask_db
todos = db.todos
yield todos
# 清理测试数据
todos.delete_many({})
client.close()
def test_get_todos_empty(client, mongo_client):
# 测试空数据库情况
response = client.get('/api/todos')
assert response.status_code == 200
assert response.json == []
def test_create_todo(client, mongo_client):
# 测试创建待办事项
test_todo = {
'content': '测试待办事项',
'degree': 1
}
response = client.post('/api/todos', json=test_todo)
assert response.status_code == 201
assert '_id' in response.json
# 验证数据是否正确保存
saved_todo = mongo_client.find_one({'_id': ObjectId(response.json['_id'])})
assert saved_todo is not None
assert saved_todo['content'] == test_todo['content']
assert saved_todo['degree'] == test_todo['degree']
assert 'timestamp' in saved_todo
def test_delete_todo(client, mongo_client):
# 先创建一个待办事项
todo = {
'content': '要删除的待办事项',
'degree': 1,
'timestamp': datetime.now()
}
result = mongo_client.insert_one(todo)
todo_id = str(result.inserted_id)
# 测试删除
response = client.delete(f'/api/todos/{todo_id}')
assert response.status_code == 204
# 验证是否已删除
assert mongo_client.find_one({'_id': ObjectId(todo_id)}) is None
def test_delete_nonexistent_todo(client):
# 测试删除不存在的待办事项
fake_id = str(ObjectId())
response = client.delete(f'/api/todos/{fake_id}')
assert response.status_code == 204 # 即使不存在也返回204

View file

@ -0,0 +1,42 @@
import pytest
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure
@pytest.fixture
def test_mongo_client():
client = MongoClient('localhost', 27017)
try:
# 测试连接是否成功
client.admin.command('ping')
db = client.test_health_check_db
collection = db.test_collection
yield collection
except ConnectionError:
pytest.fail("MongoDB连接失败 - 请确保MongoDB服务正在运行")
finally:
# 清理测试数据
if 'test_health_check_db' in client.list_database_names():
client.drop_database('test_health_check_db')
client.close()
def test_mongodb_connection_and_operations(test_mongo_client):
# 测试数据插入
test_doc = {"test_key": "test_value"}
insert_result = test_mongo_client.insert_one(test_doc)
assert insert_result.inserted_id is not None
# 测试数据查询
found_doc = test_mongo_client.find_one({"test_key": "test_value"})
assert found_doc is not None
assert found_doc["test_key"] == "test_value"
# 测试数据更新
update_result = test_mongo_client.update_one(
{"test_key": "test_value"},
{"$set": {"test_key": "updated_value"}}
)
assert update_result.modified_count == 1
# 测试数据删除
delete_result = test_mongo_client.delete_one({"test_key": "updated_value"})
assert delete_result.deleted_count == 1