feat: Migrate to MySQL and add unified environment configuration

## Database Migration (SQLite → MySQL)
- Add Alembic migration framework
- Add 'tr_' prefix to all tables to avoid conflicts in shared database
- Remove SQLite support, use MySQL exclusively
- Add pymysql driver dependency
- Change ad_token column to Text type for long JWT tokens

## Unified Environment Configuration
- Centralize all hardcoded settings to environment variables
- Backend: Extend Settings class in app/core/config.py
- Frontend: Use Vite environment variables (import.meta.env)
- Docker: Move credentials to environment variables
- Update .env.example files with comprehensive documentation

## Test Organization
- Move root-level test files to tests/ directory:
  - test_chat_room.py → tests/test_chat_room.py
  - test_websocket.py → tests/test_websocket.py
  - test_realtime_implementation.py → tests/test_realtime_implementation.py
- Fix path references in test_realtime_implementation.py

Breaking Changes:
- CORS now requires explicit origins (no more wildcard)
- All database tables renamed with 'tr_' prefix
- SQLite no longer supported

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-07 14:15:11 +08:00
parent 1d5d4d447d
commit 92834dbe0e
39 changed files with 1558 additions and 136 deletions

169
tests/test_chat_room.py Normal file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Test script for chat room management API"""
import json
import uuid
from datetime import datetime
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.database import Base
from app.modules.auth.models import UserSession
from app.modules.chat_room.models import IncidentRoom, RoomMember, RoomTemplate, RoomStatus, MemberRole
from app.modules.chat_room.services.room_service import room_service
from app.modules.chat_room.services.membership_service import membership_service
from app.modules.chat_room.services.template_service import template_service
from app.modules.chat_room.schemas import CreateRoomRequest, IncidentType, SeverityLevel
# Setup test database
engine = create_engine("sqlite:///./test_chat_room.db")
Base.metadata.create_all(bind=engine)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def test_room_management():
"""Test the core room management functionality"""
db = SessionLocal()
print("=" * 60)
print("Testing Chat Room Management System")
print("=" * 60)
try:
# Test users
admin_email = "ymirliu@panjit.com.tw"
user1_email = "john.doe@panjit.com.tw"
user2_email = "jane.smith@panjit.com.tw"
# 1. Test admin check
print("\n1. Testing admin identification:")
is_admin = membership_service.is_system_admin(admin_email)
print(f" - Is {admin_email} admin? {is_admin}")
is_admin = membership_service.is_system_admin(user1_email)
print(f" - Is {user1_email} admin? {is_admin}")
# 2. Create a room
print("\n2. Creating a new incident room:")
room_data = CreateRoomRequest(
title="Equipment Failure in Line A",
incident_type=IncidentType.EQUIPMENT_FAILURE,
severity=SeverityLevel.HIGH,
location="Production Floor A",
description="Main conveyor belt stopped working"
)
room = room_service.create_room(db, user1_email, room_data)
print(f" - Room created: {room.title}")
print(f" - Room ID: {room.room_id}")
print(f" - Created by: {room.created_by}")
print(f" - Status: {room.status}")
# 3. Check initial membership
print("\n3. Checking initial membership:")
role = membership_service.get_user_role_in_room(db, room.room_id, user1_email)
print(f" - {user1_email} role: {role}")
# 4. Add members
print("\n4. Adding members to the room:")
member2 = membership_service.add_member(
db, room.room_id, user2_email, MemberRole.EDITOR, user1_email
)
print(f" - Added {user2_email} as {member2.role}")
member3 = membership_service.add_member(
db, room.room_id, admin_email, MemberRole.VIEWER, user1_email
)
print(f" - Added {admin_email} as {member3.role}")
# 5. List room members
print("\n5. Listing all room members:")
members = membership_service.get_room_members(db, room.room_id)
for member in members:
print(f" - {member.user_id}: {member.role}")
# 6. Test ownership transfer
print("\n6. Testing ownership transfer:")
print(f" - Current owner: {user1_email}")
print(f" - Transferring to: {user2_email}")
success = membership_service.transfer_ownership(
db, room.room_id, user1_email, user2_email
)
if success:
print(" - Transfer successful!")
# Check new roles
role1 = membership_service.get_user_role_in_room(db, room.room_id, user1_email)
role2 = membership_service.get_user_role_in_room(db, room.room_id, user2_email)
print(f" - {user1_email} new role: {role1}")
print(f" - {user2_email} new role: {role2}")
# Check audit fields (refetch room)
db.refresh(room)
print(f" - Transfer recorded at: {room.ownership_transferred_at}")
print(f" - Transfer by: {room.ownership_transferred_by}")
# 7. Test admin override
print("\n7. Testing admin override capabilities:")
# Admin can update room even if only a viewer
from app.modules.chat_room.schemas import UpdateRoomRequest
update_data = UpdateRoomRequest(
severity=SeverityLevel.CRITICAL,
status=RoomStatus.RESOLVED,
resolution_notes="Admin resolved the issue"
)
# Simulate admin update (in real API, this would be checked in dependencies)
is_admin = membership_service.is_system_admin(admin_email)
if is_admin:
print(f" - Admin {admin_email} updating room (has override)")
room = room_service.update_room(db, room.room_id, update_data)
print(f" - New severity: {room.severity}")
print(f" - New status: {room.status}")
print(f" - Resolution notes: {room.resolution_notes}")
# 8. Test room templates
print("\n8. Testing room templates:")
templates = template_service.get_templates(db)
print(f" - Found {len(templates)} templates")
for template in templates:
print(f" * {template.name}: {template.incident_type}")
if templates:
# Create room from template
print("\n9. Creating room from template:")
template = templates[0]
room2 = template_service.create_room_from_template(
db,
template.template_id,
user1_email,
title="New Equipment Issue",
location="Line B",
description="Using template for quick setup"
)
print(f" - Room created from template: {room2.title}")
print(f" - Incident type: {room2.incident_type}")
print(f" - Severity: {room2.severity}")
# Check default members
members = membership_service.get_room_members(db, room2.room_id)
print(f" - Auto-added {len(members)} members from template")
# 10. Test user's room listing
print("\n10. Testing user's room listing:")
from app.modules.chat_room.schemas import RoomFilterParams
filters = RoomFilterParams(limit=10, offset=0)
# User1's rooms
rooms, total = room_service.list_user_rooms(db, user1_email, filters, False)
print(f" - {user1_email} has access to {total} room(s)")
# Admin's rooms (with admin flag)
rooms, total = room_service.list_user_rooms(db, admin_email, filters, True)
print(f" - Admin {admin_email} can see {total} room(s)")
print("\n" + "=" * 60)
print("All tests completed successfully!")
print("=" * 60)
except Exception as e:
print(f"\nError during testing: {e}")
import traceback
traceback.print_exc()
finally:
db.close()
if __name__ == "__main__":
test_room_management()

View File

@@ -0,0 +1,409 @@
#!/usr/bin/env python3
"""
Complete test suite for realtime messaging implementation
Tests all core functionality against tasks.md requirements
"""
import sys
import os
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import inspect
from app.core.database import engine, SessionLocal
from app.modules.realtime.models import Message, MessageReaction, MessageEditHistory, MessageType
from app.modules.realtime.services.message_service import MessageService
from app.modules.chat_room.models import IncidentRoom, RoomMember, MemberRole, IncidentType, SeverityLevel, RoomStatus
from datetime import datetime
import uuid
def print_section(title):
"""Print formatted section header"""
print("\n" + "=" * 60)
print(f" {title}")
print("=" * 60)
def test_database_schema():
"""Test 1: Database Schema (tasks.md section 1)"""
print_section("測試 1: 資料庫架構")
inspector = inspect(engine)
tables = inspector.get_table_names()
# 1.1 Check messages table
print("\n1.1 檢查 messages 表...")
assert "messages" in tables, "❌ messages table not found"
columns = {col['name']: col for col in inspector.get_columns('messages')}
required_columns = ['message_id', 'room_id', 'sender_id', 'content',
'message_type', 'message_metadata', 'created_at',
'edited_at', 'deleted_at', 'sequence_number']
for col in required_columns:
assert col in columns, f"❌ Column {col} not found"
print(f"✓ messages 表包含所有必要欄位: {', '.join(required_columns)}")
# 1.2 Check message_reactions table
print("\n1.2 檢查 message_reactions 表...")
assert "message_reactions" in tables, "❌ message_reactions table not found"
columns = {col['name']: col for col in inspector.get_columns('message_reactions')}
required_columns = ['reaction_id', 'message_id', 'user_id', 'emoji', 'created_at']
for col in required_columns:
assert col in columns, f"❌ Column {col} not found"
print(f"✓ message_reactions 表包含所有必要欄位: {', '.join(required_columns)}")
# 1.3 Check message_edit_history table
print("\n1.3 檢查 message_edit_history 表...")
assert "message_edit_history" in tables, "❌ message_edit_history table not found"
columns = {col['name']: col for col in inspector.get_columns('message_edit_history')}
required_columns = ['edit_id', 'message_id', 'original_content', 'edited_by', 'edited_at']
for col in required_columns:
assert col in columns, f"❌ Column {col} not found"
print(f"✓ message_edit_history 表包含所有必要欄位: {', '.join(required_columns)}")
# 1.4 Check indexes
print("\n1.4 檢查索引...")
indexes = inspector.get_indexes('messages')
index_names = [idx['name'] for idx in indexes]
required_indexes = ['ix_messages_room_created', 'ix_messages_room_sequence', 'ix_messages_sender']
for idx in required_indexes:
assert idx in index_names, f"❌ Index {idx} not found"
print(f"✓ 所有必要索引已建立: {', '.join(required_indexes)}")
print("\n✅ 資料庫架構測試通過!")
return True
def test_message_models():
"""Test 3.1: Message Models and Schemas"""
print_section("測試 3.1: 訊息模型與 Schema")
db = SessionLocal()
try:
# Create test room first
test_room = IncidentRoom(
room_id=str(uuid.uuid4()),
title="Test Room for Messages",
incident_type=IncidentType.EQUIPMENT_FAILURE,
severity=SeverityLevel.MEDIUM,
status=RoomStatus.ACTIVE,
created_by="test@example.com"
)
db.add(test_room)
db.commit()
# 3.1.1 Test Message model (via service)
print("\n3.1.1 測試 Message 模型...")
message = MessageService.create_message(
db=db,
room_id=test_room.room_id,
sender_id="test@example.com",
content="Test message",
message_type=MessageType.TEXT,
metadata={"test": "data"}
)
print(f"✓ Message 模型建立成功: {message.message_id}")
# Test all message types (via service)
print("\n3.1.3 測試訊息類型枚舉...")
message_types = [MessageType.TEXT, MessageType.IMAGE_REF, MessageType.FILE_REF,
MessageType.SYSTEM, MessageType.INCIDENT_DATA]
for msg_type in message_types:
MessageService.create_message(
db=db,
room_id=test_room.room_id,
sender_id="test@example.com",
content=f"Test {msg_type.value}",
message_type=msg_type
)
print(f"✓ 所有訊息類型支援: {[t.value for t in message_types]}")
# 3.1.1 Test MessageReaction model
print("\n3.1.1 測試 MessageReaction 模型...")
reaction = MessageReaction(
message_id=message.message_id,
user_id="test@example.com",
emoji="👍"
)
db.add(reaction)
db.commit()
print(f"✓ MessageReaction 模型建立成功: {reaction.emoji}")
# 3.1.1 Test MessageEditHistory model
print("\n3.1.1 測試 MessageEditHistory 模型...")
edit_history = MessageEditHistory(
message_id=message.message_id,
original_content="Original content",
edited_by="test@example.com"
)
db.add(edit_history)
db.commit()
print(f"✓ MessageEditHistory 模型建立成功")
print("\n✅ 訊息模型測試通過!")
return True
finally:
# Cleanup
db.query(MessageEditHistory).delete()
db.query(MessageReaction).delete()
db.query(Message).delete()
db.query(IncidentRoom).filter(IncidentRoom.room_id == test_room.room_id).delete()
db.commit()
db.close()
def test_message_service():
"""Test 4: Message Service Layer"""
print_section("測試 4: 訊息服務層")
db = SessionLocal()
try:
# Create test room and member
test_room = IncidentRoom(
room_id=str(uuid.uuid4()),
title="Test Room for Service",
incident_type=IncidentType.QUALITY_ISSUE,
severity=SeverityLevel.HIGH,
status=RoomStatus.ACTIVE,
created_by="test@example.com"
)
db.add(test_room)
db.commit()
# 4.1.1 Test create_message
print("\n4.1.1 測試 create_message...")
message = MessageService.create_message(
db=db,
room_id=test_room.room_id,
sender_id="test@example.com",
content="Test message from service",
message_type=MessageType.TEXT,
metadata={"mentions": ["user1@example.com"]}
)
assert message.message_id is not None
assert message.sequence_number > 0
print(f"✓ 訊息建立成功: ID={message.message_id}, Seq={message.sequence_number}")
# 4.1.1 Test get_messages with pagination
print("\n4.1.1 測試 get_messages (分頁)...")
# Create more messages
for i in range(5):
MessageService.create_message(
db=db,
room_id=test_room.room_id,
sender_id="test@example.com",
content=f"Message {i}"
)
result = MessageService.get_messages(db=db, room_id=test_room.room_id, limit=3)
assert result.total == 6 # 1 original + 5 new
assert len(result.messages) == 3
assert result.has_more == True
print(f"✓ 分頁查詢成功: total={result.total}, returned={len(result.messages)}, has_more={result.has_more}")
# 4.1.1 Test edit_message
print("\n4.1.1 測試 edit_message...")
edited = MessageService.edit_message(
db=db,
message_id=message.message_id,
user_id="test@example.com",
new_content="Edited content"
)
assert edited is not None
assert edited.content == "Edited content"
assert edited.edited_at is not None
print(f"✓ 訊息編輯成功: edited_at={edited.edited_at}")
# Check edit history was created
edit_history = db.query(MessageEditHistory).filter(
MessageEditHistory.message_id == message.message_id
).first()
assert edit_history is not None
assert edit_history.original_content == "Test message from service"
print(f"✓ 編輯歷史已記錄: original_content={edit_history.original_content}")
# 4.1.1 Test delete_message (soft delete)
print("\n4.1.1 測試 delete_message (軟刪除)...")
deleted = MessageService.delete_message(
db=db,
message_id=message.message_id,
user_id="test@example.com"
)
assert deleted is not None
assert deleted.deleted_at is not None
print(f"✓ 訊息軟刪除成功: deleted_at={deleted.deleted_at}")
# Verify soft delete (should not appear in get_messages by default)
result = MessageService.get_messages(db=db, room_id=test_room.room_id, include_deleted=False)
deleted_messages = [m for m in result.messages if m.message_id == message.message_id]
assert len(deleted_messages) == 0
print(f"✓ 軟刪除訊息不出現在查詢中")
# 4.2.1 Test add_reaction
print("\n4.2.1 測試 add_reaction...")
msg2 = MessageService.create_message(
db=db,
room_id=test_room.room_id,
sender_id="test@example.com",
content="Message for reactions"
)
reaction = MessageService.add_reaction(
db=db,
message_id=msg2.message_id,
user_id="user1@example.com",
emoji="👍"
)
assert reaction is not None
print(f"✓ 反應新增成功: {reaction.emoji}")
# 4.2.1 Test get_message_reactions
print("\n4.2.1 測試 get_message_reactions...")
MessageService.add_reaction(db=db, message_id=msg2.message_id, user_id="user2@example.com", emoji="👍")
MessageService.add_reaction(db=db, message_id=msg2.message_id, user_id="user3@example.com", emoji="❤️")
reactions = MessageService.get_message_reactions(db=db, message_id=msg2.message_id)
assert len(reactions) == 2 # 👍 and ❤️
thumbs_up = [r for r in reactions if r.emoji == "👍"][0]
assert thumbs_up.count == 2
print(f"✓ 反應統計成功: 👍={thumbs_up.count}, ❤️={len([r for r in reactions if r.emoji == '❤️'])}")
# 4.1.1 Test search_messages
print("\n4.1.1 測試 search_messages...")
MessageService.create_message(db=db, room_id=test_room.room_id, sender_id="test@example.com",
content="Equipment failure detected")
result = MessageService.search_messages(db=db, room_id=test_room.room_id, query="Equipment")
assert result.total >= 1
print(f"✓ 訊息搜尋成功: found {result.total} messages")
print("\n✅ 訊息服務層測試通過!")
return True
finally:
# Cleanup
db.query(MessageEditHistory).delete()
db.query(MessageReaction).delete()
db.query(Message).delete()
db.query(IncidentRoom).filter(IncidentRoom.room_id == test_room.room_id).delete()
db.commit()
db.close()
def test_websocket_manager():
"""Test 2: WebSocket Infrastructure"""
print_section("測試 2: WebSocket 基礎架構")
from app.modules.realtime.websocket_manager import WebSocketManager
manager = WebSocketManager()
# 2.1.2 Test manager methods exist
print("\n2.1.2 檢查 WebSocketManager 方法...")
required_methods = ['connect', 'disconnect', 'broadcast_to_room',
'send_personal', 'get_room_connections', 'get_online_users',
'is_user_online', 'set_typing', 'get_typing_users']
for method in required_methods:
assert hasattr(manager, method), f"❌ Method {method} not found"
print(f"✓ 所有必要方法已實作: {', '.join(required_methods)}")
# Test data structures
print("\n2.1.2 檢查內部資料結構...")
assert hasattr(manager, '_room_connections')
assert hasattr(manager, '_user_connections')
assert hasattr(manager, '_typing_users')
print(f"✓ 連線池資料結構已初始化")
print("\n✅ WebSocket 基礎架構測試通過!")
return True
def test_module_structure():
"""Test 2.1.1: Module Structure"""
print_section("測試 2.1.1: 模組結構")
import os
base_path = "app/modules/realtime"
required_files = [
'__init__.py',
'websocket_manager.py',
'models.py',
'schemas.py',
'router.py',
'services/message_service.py'
]
for file in required_files:
file_path = os.path.join(base_path, file)
assert os.path.exists(file_path), f"❌ File {file_path} not found"
print(f"✓ 所有必要檔案已建立: {', '.join(required_files)}")
# Test imports
print("\n測試模組匯入...")
from app.modules.realtime import Message, MessageReaction, MessageEditHistory, MessageType, router
from app.modules.realtime.websocket_manager import manager
from app.modules.realtime.services.message_service import MessageService
from app.modules.realtime import schemas
print("✓ 所有模組成功匯入")
print("\n✅ 模組結構測試通過!")
return True
def main():
"""Run all tests"""
print("\n" + "=" * 60)
print(" 即時訊息功能完整測試")
print(" Testing Realtime Messaging Implementation")
print("=" * 60)
tests = [
("模組結構", test_module_structure),
("資料庫架構", test_database_schema),
("WebSocket 基礎架構", test_websocket_manager),
("訊息模型", test_message_models),
("訊息服務層", test_message_service),
]
passed = 0
failed = 0
for name, test_func in tests:
try:
test_func()
passed += 1
except AssertionError as e:
print(f"\n{name} 測試失敗: {e}")
failed += 1
except Exception as e:
print(f"\n{name} 測試錯誤: {e}")
import traceback
traceback.print_exc()
failed += 1
# Final summary
print("\n" + "=" * 60)
print(" 測試總結 (Test Summary)")
print("=" * 60)
print(f"通過: {passed}/{len(tests)}")
print(f"失敗: {failed}/{len(tests)}")
if failed == 0:
print("\n🎉 所有測試通過!")
print("\n已完成核心功能:")
print(" ✓ 資料庫架構 (Section 1)")
print(" ✓ WebSocket 基礎架構 (Section 2)")
print(" ✓ 訊息處理 (Section 3)")
print(" ✓ 訊息服務層 (Section 4)")
print(" ✓ REST API 端點 (Section 5)")
return 0
else:
print(f"\n⚠️ 有 {failed} 個測試失敗")
return 1
if __name__ == "__main__":
sys.exit(main())

98
tests/test_websocket.py Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Test WebSocket realtime messaging functionality"""
import asyncio
import websockets
import json
from datetime import datetime
async def test_websocket_connection():
"""Test basic WebSocket connection and message sending"""
# Note: This is a simplified test. In production, you'd need proper authentication
uri = "ws://localhost:8000/api/ws/test-room-123?token=test-user@example.com"
print("Connecting to WebSocket...")
try:
async with websockets.connect(uri) as websocket:
print("✓ Connected to WebSocket!")
# Send a text message
message = {
"type": "message",
"content": "Hello from WebSocket test!",
"message_type": "text"
}
print(f"\nSending message: {message}")
await websocket.send(json.dumps(message))
# Wait for acknowledgment
response = await asyncio.wait_for(websocket.recv(), timeout=5)
response_data = json.loads(response)
print(f"✓ Received response: {response_data}")
if response_data.get("type") == "ack":
print(f" Message ID: {response_data.get('message_id')}")
print(f" Sequence: {response_data.get('sequence_number')}")
except websockets.exceptions.InvalidStatusCode as e:
if e.status_code == 4001:
print("✗ Connection rejected: Not a member of room")
print(" This is expected for test room without proper membership")
else:
print(f"✗ Connection failed with status {e.status_code}")
except Exception as e:
print(f"✗ Error: {e}")
async def test_rest_api():
"""Test REST API endpoints"""
import aiohttp
print("\n" + "=" * 50)
print("Testing REST API Endpoints")
print("=" * 50)
# Note: This requires authentication in production
# For now, just test if endpoints are registered
async with aiohttp.ClientSession() as session:
# Test health check
async with session.get("http://localhost:8000/health") as resp:
if resp.status == 200:
print("✓ Health check endpoint working")
else:
print(f"✗ Health check failed: {resp.status}")
# Test API docs
async with session.get("http://localhost:8000/docs") as resp:
if resp.status == 200:
print("✓ API documentation accessible")
else:
print(f"✗ API docs failed: {resp.status}")
if __name__ == "__main__":
print("=" * 50)
print("WebSocket Realtime Messaging Test")
print("=" * 50)
# Run REST API tests
asyncio.run(test_rest_api())
print("\n" + "=" * 50)
print("Testing WebSocket Connection")
print("=" * 50)
# Run WebSocket test
asyncio.run(test_websocket_connection())
print("\n" + "=" * 50)
print("Test Summary:")
print("- Database tables created ✓")
print("- Models and schemas defined ✓")
print("- WebSocket manager implemented ✓")
print("- Message service layer created ✓")
print("- REST API endpoints registered ✓")
print("- Server starts successfully ✓")
print("=" * 50)