Skip to content

Commit d345a63

Browse files
committed
Update readme
1 parent f648af6 commit d345a63

File tree

6 files changed

+234
-46
lines changed

6 files changed

+234
-46
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,5 @@ cython_debug/
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
#.idea/
161-
/temp/
161+
/temp/
162+
/*.json

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
[中文](README_zh.md)
2+
13
# Electerm sync server python
24

35
[![Build Status](https://github.com/electerm/electerm-sync-server-python/actions/workflows/linux.yml/badge.svg)](https://github.com/electerm/electerm-sync-server-python/actions)
46

7+
8+
59
A simple electerm data sync server with python.
610

711
## Use
@@ -11,6 +15,11 @@ Requires python3
1115
```bash
1216
git clone git@github.com:electerm/electerm-sync-server-python.git
1317
cd electerm-sync-server-python
18+
python -m venv venv
19+
# On Windows (PowerShell):
20+
venv\Scripts\activate
21+
# On Unix/Mac:
22+
# source venv/bin/activate
1423
pip install -r requirements.txt
1524

1625
# create env file, then edit .env

README_zh.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[English](README.md)
2+
3+
# Electerm 同步服务器 Python 版
4+
5+
[![Build Status](https://github.com/electerm/electerm-sync-server-python/actions/workflows/linux.yml/badge.svg)](https://github.com/electerm/electerm-sync-server-python/actions)
6+
7+
一个简单的 Electerm 数据同步服务器,使用 Python。
8+
9+
## 使用
10+
11+
需要 Python 3
12+
13+
```bash
14+
git clone git@github.com:electerm/electerm-sync-server-python.git
15+
cd electerm-sync-server-python
16+
python -m venv venv
17+
# 在 Windows (PowerShell) 上:
18+
venv\Scripts\activate
19+
# 在 Unix/Mac 上:
20+
# source venv/bin/activate
21+
pip install -r requirements.txt
22+
23+
# 创建环境文件,然后编辑 .env
24+
cp sample.env .env
25+
26+
python src/app.py
27+
28+
# 会显示类似内容
29+
# server running at http://127.0.0.1:7837
30+
31+
# 在 Electerm 同步设置中,设置自定义同步服务器:
32+
# 服务器 URL:http://127.0.0.1:7837
33+
# 然后你可以在 Electerm 自定义同步中使用 http://127.0.0.1:7837/api/sync 作为 API URL
34+
35+
# JWT_SECRET:在 .env 中的 JWT_SECRET
36+
# JWT_USER_NAME:在 .env 中的一个 JWT_USER
37+
```
38+
39+
## 测试
40+
41+
```bash
42+
bin/test
43+
```
44+
45+
## 编写自己的数据存储
46+
47+
[src/file_store.py](src/file_store.py) 为例,编写自己的读写方法
48+
49+
## 其他语言的同步服务器
50+
51+
[https://github.com/electerm/electerm/wiki/Custom-sync-server](https://github.com/electerm/electerm/wiki/Custom-sync-server)
52+
53+
---
54+
55+

bin/release

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/app.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,24 @@
1111
app.config['JWT_IDENTITY_CLAIM'] = 'id'
1212
jwt = JWTManager(app)
1313

14-
@app.route('/api/sync', methods=['GET', 'PUT'])
14+
@jwt.invalid_token_loader
15+
def invalid_token_callback(error_string):
16+
return jsonify({'status': 'error', 'message': 'Invalid token'}), 422
17+
18+
@jwt.expired_token_loader
19+
def expired_token_callback(jwt_header, jwt_payload):
20+
return jsonify({'status': 'error', 'message': 'Token expired'}), 422
21+
22+
@jwt.unauthorized_loader
23+
def unauthorized_callback(error_string):
24+
return jsonify({'status': 'error', 'message': 'Missing or invalid token'}), 422
25+
26+
@app.route('/api/sync', methods=['GET', 'PUT', 'POST'])
1527
@jwt_required()
1628
def sync():
1729
user_id = get_jwt_identity()
1830
users = os.environ['JWT_USERS'].split(',')
31+
1932
if user_id not in users:
2033
return jsonify({'status': 'error', 'message': 'Unauthorized!'}), 401
2134

tests/test.py

Lines changed: 154 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,179 @@
33
import tempfile
44
import os
55
import sys
6+
import shutil
7+
from unittest.mock import patch
68
from dotenv import load_dotenv
79

10+
# Load environment variables
811
load_dotenv()
912
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
1013

14+
from src import file_store
1115
from src.file_store import read, write
1216

13-
class TestExample(unittest.TestCase):
17+
class TestFileStore(unittest.TestCase):
1418

1519
def setUp(self):
16-
self.test_user_id = 'test_user_id'
17-
self.test_data = {'key': 'value'}
20+
# Create temporary directory for testing
21+
self.temp_dir = tempfile.mkdtemp(prefix='test_file_store_')
1822

19-
def test_write(self):
20-
# Use temporary file for testing
21-
with tempfile.NamedTemporaryFile() as f:
23+
# Set FILE_STORE_PATH to our temp directory
24+
self.env_patcher = patch.dict(os.environ, {'FILE_STORE_PATH': self.temp_dir})
25+
self.env_patcher.start()
2226

23-
# Write to file
24-
write_status, write_code = write(self.test_data, self.test_user_id)
27+
# Re-import file_store to pick up the new environment variable
28+
import importlib
29+
importlib.reload(file_store)
30+
from src.file_store import read, write
31+
globals()['read'] = read
32+
globals()['write'] = write
2533

26-
# Test status and code
27-
self.assertEqual(write_status, 'ok')
28-
self.assertEqual(write_code, 200)
34+
# Test data
35+
self.test_user_id = 'testuser1'
36+
self.test_data = {'key': 'value', 'data': [1, 2, 3]}
37+
self.empty_data = {}
38+
self.none_data = None
2939

30-
# Read from file
31-
read_data, read_code = read(self.test_user_id)
40+
def tearDown(self):
41+
self.env_patcher.stop()
42+
# Clean up temp directory
43+
shutil.rmtree(self.temp_dir, ignore_errors=True)
3244

33-
# Test read data and code
34-
self.assertEqual(read_data, self.test_data)
35-
self.assertEqual(read_code, 200)
45+
def test_write_valid_data(self):
46+
"""Test writing valid data"""
47+
status, code = write(self.test_data, self.test_user_id)
48+
self.assertEqual(status, 'ok')
49+
self.assertEqual(code, 200)
3650

37-
def test_read_not_found(self):
38-
# Use temporary file for testing
39-
with tempfile.TemporaryDirectory() as tempdir:
40-
os.environ['FILE_STORE_PATH'] = tempdir
51+
# Verify file was created and contains correct data
52+
data, read_code = read(self.test_user_id)
53+
self.assertEqual(read_code, 200)
54+
self.assertEqual(data, self.test_data)
4155

42-
# Read from unexisting file
43-
read_data, read_code = read(self.test_user_id + '0000')
56+
def test_write_empty_data(self):
57+
"""Test writing empty dictionary"""
58+
status, code = write(self.empty_data, self.test_user_id)
59+
self.assertEqual(status, 'ok')
60+
self.assertEqual(code, 200)
4461

45-
# Test not found data and code
46-
self.assertEqual(read_data, 'File not found')
47-
self.assertEqual(read_code, 404)
62+
data, read_code = read(self.test_user_id)
63+
self.assertEqual(read_code, 200)
64+
self.assertEqual(data, {})
4865

49-
def test_read(self):
50-
# Use temporary file for testing
51-
with tempfile.TemporaryDirectory() as tempdir:
52-
# Read from unexisting file
53-
write(self.test_data, self.test_user_id)
54-
read_data, read_code = read(self.test_user_id)
66+
def test_write_none_data(self):
67+
"""Test writing None data (should be converted to empty dict)"""
68+
status, code = write(self.none_data, self.test_user_id)
69+
self.assertEqual(status, 'ok')
70+
self.assertEqual(code, 200)
5571

56-
# Test not found data and code
57-
self.assertEqual(read_data, self.test_data)
58-
self.assertEqual(read_code, 200)
72+
data, read_code = read(self.test_user_id)
73+
self.assertEqual(read_code, 200)
74+
self.assertEqual(data, {})
75+
76+
def test_write_complex_data(self):
77+
"""Test writing complex nested data"""
78+
complex_data = {
79+
'users': [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}],
80+
'settings': {'theme': 'dark', 'notifications': True},
81+
'timestamp': '2025-11-10T16:59:40'
82+
}
83+
status, code = write(complex_data, self.test_user_id)
84+
self.assertEqual(status, 'ok')
85+
self.assertEqual(code, 200)
86+
87+
data, read_code = read(self.test_user_id)
88+
self.assertEqual(read_code, 200)
89+
self.assertEqual(data, complex_data)
90+
91+
def test_read_existing_file(self):
92+
"""Test reading existing file"""
93+
write(self.test_data, self.test_user_id)
94+
data, code = read(self.test_user_id)
95+
self.assertEqual(code, 200)
96+
self.assertEqual(data, self.test_data)
97+
98+
def test_read_nonexistent_file(self):
99+
"""Test reading non-existent file"""
100+
data, code = read('nonexistent_user')
101+
self.assertEqual(code, 404)
102+
self.assertEqual(data, 'File not found')
103+
104+
def test_read_after_write_overwrite(self):
105+
"""Test overwriting existing data"""
106+
# Write initial data
107+
write({'initial': 'data'}, self.test_user_id)
108+
109+
# Overwrite with new data
110+
new_data = {'updated': 'data', 'version': 2}
111+
write(new_data, self.test_user_id)
112+
113+
# Read should return new data
114+
data, code = read(self.test_user_id)
115+
self.assertEqual(code, 200)
116+
self.assertEqual(data, new_data)
117+
118+
def test_multiple_users_isolation(self):
119+
"""Test that different users have isolated data"""
120+
user1_data = {'user': 'user1', 'value': 100}
121+
user2_data = {'user': 'user2', 'value': 200}
122+
123+
write(user1_data, 'user1')
124+
write(user2_data, 'user2')
125+
126+
data1, code1 = read('user1')
127+
data2, code2 = read('user2')
128+
129+
self.assertEqual(code1, 200)
130+
self.assertEqual(code2, 200)
131+
self.assertEqual(data1, user1_data)
132+
self.assertEqual(data2, user2_data)
133+
self.assertNotEqual(data1, data2)
134+
135+
def test_file_path_construction(self):
136+
"""Test that files are created in the correct location"""
137+
write(self.test_data, self.test_user_id)
138+
139+
# Check if file exists in temp directory
140+
expected_path = os.path.join(self.temp_dir, f"{self.test_user_id}.json")
141+
self.assertTrue(os.path.exists(expected_path))
142+
143+
# Verify file contents
144+
with open(expected_path, 'r') as f:
145+
file_data = json.load(f)
146+
self.assertEqual(file_data, self.test_data)
147+
148+
def test_write_creates_directory_if_needed(self):
149+
"""Test that write creates necessary directories"""
150+
# This test assumes FILE_STORE_PATH is set to a temp directory
151+
# The write function should work even if subdirectories don't exist
152+
status, code = write(self.test_data, self.test_user_id)
153+
self.assertEqual(status, 'ok')
154+
self.assertEqual(code, 200)
155+
156+
def test_read_file_corruption(self):
157+
"""Test reading corrupted JSON file"""
158+
# Create a corrupted JSON file manually
159+
file_path = os.path.join(self.temp_dir, f"{self.test_user_id}.json")
160+
with open(file_path, 'w') as f:
161+
f.write('{"invalid": json syntax}')
162+
163+
# This should raise a JSONDecodeError since the file_store doesn't handle corrupted JSON
164+
with self.assertRaises(json.JSONDecodeError):
165+
read(self.test_user_id)
166+
167+
def test_environment_variable_file_store_path(self):
168+
"""Test that FILE_STORE_PATH environment variable is respected"""
169+
# With our mocking, files should go to temp_dir
170+
write(self.test_data, self.test_user_id)
171+
expected_path = os.path.join(self.temp_dir, f"{self.test_user_id}.json")
172+
173+
# The file should exist in the mocked temp directory
174+
self.assertTrue(os.path.exists(expected_path))
175+
176+
# Clean up
177+
if os.path.exists(expected_path):
178+
os.remove(expected_path)
59179

60180
if __name__ == '__main__':
61181
unittest.main()

0 commit comments

Comments
 (0)