205 lines
6.8 KiB
Python
205 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for POST /api/habits endpoint"""
|
|
|
|
import json
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from datetime import datetime
|
|
from http.server import HTTPServer
|
|
from pathlib import Path
|
|
from threading import Thread
|
|
from time import sleep
|
|
from urllib.request import Request, urlopen
|
|
from urllib.error import HTTPError
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from dashboard.api import TaskBoardHandler
|
|
|
|
class TestHabitsPost(unittest.TestCase):
|
|
"""Test POST /api/habits endpoint"""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
"""Start test server"""
|
|
# Create temp habits.json
|
|
cls.temp_dir = Path(tempfile.mkdtemp())
|
|
cls.habits_file = cls.temp_dir / 'habits.json'
|
|
cls.habits_file.write_text(json.dumps({
|
|
'lastUpdated': datetime.now().isoformat(),
|
|
'habits': []
|
|
}))
|
|
|
|
# Monkey-patch KANBAN_DIR
|
|
import dashboard.api as api_module
|
|
cls.original_kanban_dir = api_module.KANBAN_DIR
|
|
api_module.KANBAN_DIR = cls.temp_dir
|
|
|
|
# Start server
|
|
cls.port = 18088
|
|
cls.server = HTTPServer(('127.0.0.1', cls.port), TaskBoardHandler)
|
|
cls.thread = Thread(target=cls.server.serve_forever, daemon=True)
|
|
cls.thread.start()
|
|
sleep(0.5)
|
|
|
|
cls.base_url = f'http://127.0.0.1:{cls.port}'
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
"""Stop server and cleanup"""
|
|
cls.server.shutdown()
|
|
cls.thread.join(timeout=2)
|
|
|
|
# Restore KANBAN_DIR
|
|
import dashboard.api as api_module
|
|
api_module.KANBAN_DIR = cls.original_kanban_dir
|
|
|
|
# Cleanup temp dir
|
|
shutil.rmtree(cls.temp_dir)
|
|
|
|
def setUp(self):
|
|
"""Reset habits.json before each test"""
|
|
self.habits_file.write_text(json.dumps({
|
|
'lastUpdated': datetime.now().isoformat(),
|
|
'habits': []
|
|
}))
|
|
|
|
def post_habit(self, data):
|
|
"""Helper to POST to /api/habits"""
|
|
url = f'{self.base_url}/api/habits'
|
|
req = Request(url, data=json.dumps(data).encode(), method='POST')
|
|
req.add_header('Content-Type', 'application/json')
|
|
return urlopen(req)
|
|
|
|
def test_create_habit_success(self):
|
|
"""Test creating a valid habit"""
|
|
data = {'name': 'Bazin', 'frequency': 'daily'}
|
|
resp = self.post_habit(data)
|
|
|
|
self.assertEqual(resp.status, 201)
|
|
|
|
result = json.loads(resp.read())
|
|
self.assertIn('id', result)
|
|
self.assertTrue(result['id'].startswith('habit-'))
|
|
self.assertEqual(result['name'], 'Bazin')
|
|
self.assertEqual(result['frequency'], 'daily')
|
|
self.assertIn('createdAt', result)
|
|
self.assertEqual(result['completions'], [])
|
|
|
|
# Verify ISO timestamp
|
|
datetime.fromisoformat(result['createdAt'])
|
|
|
|
def test_habit_persisted_to_file(self):
|
|
"""Test habit is written to habits.json"""
|
|
data = {'name': 'Sală', 'frequency': 'weekly'}
|
|
resp = self.post_habit(data)
|
|
habit = json.loads(resp.read())
|
|
|
|
# Read file
|
|
file_data = json.loads(self.habits_file.read_text())
|
|
self.assertEqual(len(file_data['habits']), 1)
|
|
self.assertEqual(file_data['habits'][0]['id'], habit['id'])
|
|
self.assertEqual(file_data['habits'][0]['name'], 'Sală')
|
|
|
|
def test_id_format_correct(self):
|
|
"""Test generated id follows 'habit-{timestamp}' format"""
|
|
data = {'name': 'Test', 'frequency': 'daily'}
|
|
resp = self.post_habit(data)
|
|
habit = json.loads(resp.read())
|
|
|
|
habit_id = habit['id']
|
|
self.assertTrue(habit_id.startswith('habit-'))
|
|
|
|
# Extract timestamp and verify it's numeric
|
|
timestamp_part = habit_id.replace('habit-', '')
|
|
self.assertTrue(timestamp_part.isdigit())
|
|
|
|
# Verify timestamp is reasonable (milliseconds since epoch)
|
|
timestamp_ms = int(timestamp_part)
|
|
now_ms = int(datetime.now().timestamp() * 1000)
|
|
# Should be within 5 seconds
|
|
self.assertLess(abs(now_ms - timestamp_ms), 5000)
|
|
|
|
def test_missing_name_returns_400(self):
|
|
"""Test missing name returns 400"""
|
|
data = {'frequency': 'daily'}
|
|
|
|
with self.assertRaises(HTTPError) as ctx:
|
|
self.post_habit(data)
|
|
|
|
self.assertEqual(ctx.exception.code, 400)
|
|
error = json.loads(ctx.exception.read())
|
|
self.assertIn('name', error['error'].lower())
|
|
|
|
def test_empty_name_returns_400(self):
|
|
"""Test empty name (whitespace only) returns 400"""
|
|
data = {'name': ' ', 'frequency': 'daily'}
|
|
|
|
with self.assertRaises(HTTPError) as ctx:
|
|
self.post_habit(data)
|
|
|
|
self.assertEqual(ctx.exception.code, 400)
|
|
|
|
def test_invalid_frequency_returns_400(self):
|
|
"""Test invalid frequency returns 400"""
|
|
data = {'name': 'Test', 'frequency': 'monthly'}
|
|
|
|
with self.assertRaises(HTTPError) as ctx:
|
|
self.post_habit(data)
|
|
|
|
self.assertEqual(ctx.exception.code, 400)
|
|
error = json.loads(ctx.exception.read())
|
|
self.assertIn('frequency', error['error'].lower())
|
|
|
|
def test_missing_frequency_returns_400(self):
|
|
"""Test missing frequency returns 400"""
|
|
data = {'name': 'Test'}
|
|
|
|
with self.assertRaises(HTTPError) as ctx:
|
|
self.post_habit(data)
|
|
|
|
self.assertEqual(ctx.exception.code, 400)
|
|
|
|
def test_multiple_habits_created(self):
|
|
"""Test creating multiple habits"""
|
|
habit1 = {'name': 'Bazin', 'frequency': 'daily'}
|
|
habit2 = {'name': 'Sală', 'frequency': 'weekly'}
|
|
|
|
resp1 = self.post_habit(habit1)
|
|
h1 = json.loads(resp1.read())
|
|
|
|
# Small delay to ensure different timestamp
|
|
sleep(0.01)
|
|
|
|
resp2 = self.post_habit(habit2)
|
|
h2 = json.loads(resp2.read())
|
|
|
|
# IDs should be different
|
|
self.assertNotEqual(h1['id'], h2['id'])
|
|
|
|
# Both should be in file
|
|
file_data = json.loads(self.habits_file.read_text())
|
|
self.assertEqual(len(file_data['habits']), 2)
|
|
|
|
def test_last_updated_timestamp(self):
|
|
"""Test lastUpdated is updated when creating habit"""
|
|
before = datetime.now().isoformat()
|
|
|
|
data = {'name': 'Test', 'frequency': 'daily'}
|
|
self.post_habit(data)
|
|
|
|
file_data = json.loads(self.habits_file.read_text())
|
|
last_updated = file_data['lastUpdated']
|
|
|
|
# Should be a valid ISO timestamp
|
|
datetime.fromisoformat(last_updated)
|
|
# Should be recent
|
|
self.assertGreaterEqual(last_updated, before)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|