- tests/test_habit_bot.py: Habit tracking, food logging, keto guidance - tests/test_stock_tracker.py: Portfolio management, P&L calculation - pytest.ini: Pytest configuration - Updated Jenkinsfile: Emphasized testing stages before build Pipeline stages: 1. Code Quality Gates (lint + security) 2. Unit Tests (pytest with coverage) 3. Integration Tests (Oracle, Telegram, Gitea) 4. Build (only after tests pass) 5. Deploy to Staging
256 lines
8.4 KiB
Python
256 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unit tests for Habit Bot
|
|
Tests: habit tracking, food logging, data persistence
|
|
"""
|
|
|
|
import pytest
|
|
import sys
|
|
import os
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
# Test data directory
|
|
TEST_DATA_DIR = '/tmp/test_habit_bot'
|
|
os.makedirs(TEST_DATA_DIR, exist_ok=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_data():
|
|
"""Create mock data for testing"""
|
|
return {
|
|
'users': {},
|
|
'habits': {},
|
|
'habit_logs': {},
|
|
'food_logs': {},
|
|
'sessions': {}
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def app_with_mock_data(mock_data):
|
|
"""Create app with mock data"""
|
|
with patch('builtins.open', side_effect=lambda f, *args, **kwargs:
|
|
(MagicMock() if 'write' in str(f) else
|
|
(MagicMock() if any(x in str(f) for x in ['users.json', 'habits.json', 'habit_logs.json', 'food_logs.json', 'sessions.json']) else open(f, *args, **kwargs)))):
|
|
pass
|
|
|
|
# Mock load_json and save_json
|
|
def mock_load_json(f):
|
|
if 'users' in str(f):
|
|
return mock_data['users']
|
|
elif 'habits' in str(f):
|
|
return mock_data['habits']
|
|
elif 'habit_logs' in str(f):
|
|
return mock_data['habit_logs']
|
|
elif 'food_logs' in str(f):
|
|
return mock_data['food_logs']
|
|
elif 'sessions' in str(f):
|
|
return mock_data['sessions']
|
|
return {}
|
|
|
|
with patch('builtins.open', side_effect=lambda f, mode='r', *args, **kwargs:
|
|
(MagicMock(__enter__=MagicMock(return_value=StringIO(json.dumps(mock_data.get(f.split('/')[-1], {}))),
|
|
__exit__=MagicMock(return_value=False)) if any(x in str(f) for x in ['users', 'habits', 'habit_logs', 'food_logs', 'sessions']) else open(f, mode, *args, **kwargs))):
|
|
with patch('habit_bot.load_json', side_effect=mock_load_json):
|
|
yield mock_data
|
|
|
|
|
|
class TestHabitBot:
|
|
"""Test habit tracking functionality"""
|
|
|
|
def test_add_habit(self, mock_data):
|
|
"""Test adding a new habit"""
|
|
habit_name = "morning workout"
|
|
|
|
# Simulate adding habit
|
|
user_id = "12345"
|
|
if user_id not in mock_data['habits']:
|
|
mock_data['habits'][user_id] = {}
|
|
|
|
mock_data['habits'][user_id][habit_name] = {
|
|
'name': habit_name,
|
|
'streak': 0,
|
|
'created_at': datetime.now().isoformat(),
|
|
'is_active': True
|
|
}
|
|
|
|
assert habit_name in mock_data['habits'][user_id]
|
|
assert mock_data['habits'][user_id][habit_name]['streak'] == 0
|
|
print(f"✅ Added habit: {habit_name}")
|
|
|
|
def test_log_habit_completion(self, mock_data):
|
|
"""Test logging habit completion"""
|
|
habit_name = "read books"
|
|
user_id = "12345"
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
# Initialize data
|
|
if user_id not in mock_data['habits']:
|
|
mock_data['habits'][user_id] = {}
|
|
mock_data['habits'][user_id][habit_name] = {'streak': 5}
|
|
|
|
if user_id not in mock_data['habit_logs']:
|
|
mock_data['habit_logs'][user_id] = {}
|
|
if today not in mock_data['habit_logs'][user_id]:
|
|
mock_data['habit_logs'][user_id][today] = []
|
|
|
|
# Log completion
|
|
mock_data['habit_logs'][user_id][today].append({
|
|
'habit_name': habit_name,
|
|
'status': 'completed',
|
|
'notes': '30 minutes reading',
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
# Update streak
|
|
mock_data['habits'][user_id][habit_name]['streak'] += 1
|
|
|
|
assert len(mock_data['habit_logs'][user_id][today]) == 1
|
|
assert mock_data['habits'][user_id][habit_name]['streak'] == 6
|
|
print(f"✅ Logged habit: {habit_name} (streak: 6)")
|
|
|
|
def test_habit_streak_calculation(self, mock_data):
|
|
"""Test streak calculation"""
|
|
user_id = "12345"
|
|
habit_name = "exercise"
|
|
|
|
# Simulate 7-day streak
|
|
mock_data['habits'][user_id] = {
|
|
habit_name: {'streak': 7}
|
|
}
|
|
|
|
assert mock_data['habits'][user_id][habit_name]['streak'] == 7
|
|
print(f"✅ Streak calculated: 7 days")
|
|
|
|
|
|
class TestFoodLogging:
|
|
"""Test food/nutrition logging functionality"""
|
|
|
|
def test_analyze_simple_food(self, mock_data):
|
|
"""Test basic food analysis"""
|
|
from habit_bot import analyze_food_text
|
|
|
|
# Test chicken analysis
|
|
result = analyze_food_text("chicken breast 200g")
|
|
|
|
assert 'calories' in result
|
|
assert 'carbs' in result
|
|
assert 'protein' in result
|
|
assert 'fat' in result
|
|
assert result['protein'] > 0
|
|
print(f"✅ Food analyzed: {result}")
|
|
|
|
def test_analyze_multiple_foods(self, mock_data):
|
|
"""Test multi-food analysis"""
|
|
from habit_bot import analyze_food_text
|
|
|
|
# Test multiple items
|
|
result = analyze_food_text("2 eggs and 1 banana")
|
|
|
|
assert result['calories'] > 0
|
|
assert result['protein'] > 0
|
|
assert 'egg' in result or result['protein'] > 0 # Eggs contribute protein
|
|
print(f"✅ Multi-food analyzed: {result}")
|
|
|
|
def test_food_log_entry(self, mock_data):
|
|
"""Test food log entry creation"""
|
|
user_id = "12345"
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
# Create food log
|
|
if user_id not in mock_data['food_logs']:
|
|
mock_data['food_logs'][user_id] = {}
|
|
if today not in mock_data['food_logs'][user_id]:
|
|
mock_data['food_logs'][user_id][today] = []
|
|
|
|
mock_data['food_logs'][user_id][today].append({
|
|
'meal_type': 'lunch',
|
|
'food_name': 'grilled chicken',
|
|
'time': '12:30',
|
|
'calories': 300,
|
|
'carbs': 0,
|
|
'protein': 50,
|
|
'fat': 8,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
assert len(mock_data['food_logs'][user_id][today]) == 1
|
|
assert mock_data['food_logs'][user_id][today][0]['calories'] == 300
|
|
print("✅ Food log entry created")
|
|
|
|
|
|
class TestKetoGuidance:
|
|
"""Test keto diet guidance"""
|
|
|
|
def test_keto_calorie_targets(self, mock_data):
|
|
"""Test keto calorie calculation"""
|
|
# Keto guidelines
|
|
protein_per_kg = 1.3 # 1.3g per kg body weight
|
|
body_weight_kg = 70 # Example weight
|
|
|
|
protein_target = protein_per_kg * body_weight_kg
|
|
max_net_carbs = 25 # 25g per day
|
|
|
|
assert protein_target == 91 # 1.3 * 70
|
|
assert max_net_carbs == 25
|
|
print(f"✅ Keto targets: Protein {protein_target}g, Carbs {max_net_carbs}g")
|
|
|
|
def test_calorie_remaining(self, mock_data):
|
|
"""Test remaining calorie calculation"""
|
|
daily_target = 2000
|
|
consumed = 750
|
|
|
|
remaining = daily_target - consumed
|
|
|
|
assert remaining == 1250
|
|
print(f"✅ Calories remaining: {remaining}")
|
|
|
|
|
|
class TestDataPersistence:
|
|
"""Test data save/load functionality"""
|
|
|
|
def test_save_and_load_habits(self, mock_data, tmp_path):
|
|
"""Test habit data persistence"""
|
|
test_file = tmp_path / "test_habits.json"
|
|
|
|
# Save
|
|
mock_data['habits']['user1'] = {
|
|
'workout': {'streak': 10},
|
|
'meditation': {'streak': 5}
|
|
}
|
|
|
|
with open(test_file, 'w') as f:
|
|
json.dump(mock_data['habits'], f)
|
|
|
|
# Load
|
|
with open(test_file, 'r') as f:
|
|
loaded = json.load(f)
|
|
|
|
assert 'user1' in loaded
|
|
assert 'workout' in loaded['user1']
|
|
assert loaded['user1']['workout']['streak'] == 10
|
|
print("✅ Data persistence verified")
|
|
|
|
|
|
class TestMotivationalQuotes:
|
|
"""Test motivational quote system"""
|
|
|
|
def test_quotes_available(self, mock_data):
|
|
"""Test that quotes are available"""
|
|
from habit_bot import MOTIVATIONAL_QUOTES
|
|
|
|
assert len(MOTIVATIONAL_QUOTES) > 0
|
|
assert all(isinstance(q, str) for q in MOTIVATIONAL_QUOTES)
|
|
assert len(q) > 10 for q in MOTIVATIONAL_QUOTES) # Quotes should have content
|
|
print(f"✅ {len(MOTIVATIONAL_QUOTES)} motivational quotes available")
|
|
|
|
|
|
# Pytest configuration
|
|
if __name__ == '__main__':
|
|
pytest.main([__file__, '-v'])
|