366 lines
12 KiB
Python
366 lines
12 KiB
Python
|
|
"""
|
||
|
|
Unit tests for the actual application functions from minimal_app.py
|
||
|
|
Tests the real business logic with proper imports
|
||
|
|
"""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import sqlite3
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
# Add project root to path
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
|
|
||
|
|
# Import functions from minimal_app
|
||
|
|
from minimal_app import (
|
||
|
|
add_tablet, add_user, loan_tablet, return_tablet,
|
||
|
|
show_available_tablets, show_active_loans, show_loan_history,
|
||
|
|
add_non_loanable_device, show_non_loanable_devices, delete_non_loanable_device
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def test_db_path():
|
||
|
|
"""Path to test database"""
|
||
|
|
return 'test_minimal_app.db'
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def init_test_db(test_db_path):
|
||
|
|
"""Initialize a fresh test database with schema (same as minimal_app)"""
|
||
|
|
# Remove existing test database if it exists
|
||
|
|
if os.path.exists(test_db_path):
|
||
|
|
os.remove(test_db_path)
|
||
|
|
|
||
|
|
# Use the same init_db function from minimal_app
|
||
|
|
from minimal_app import init_db
|
||
|
|
# Temporarily rename the database
|
||
|
|
original_db = 'tablets.db'
|
||
|
|
if os.path.exists(original_db):
|
||
|
|
os.rename(original_db, f'{original_db}.backup')
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Create test database
|
||
|
|
os.environ['TEST_DB'] = test_db_path
|
||
|
|
init_db()
|
||
|
|
yield test_db_path
|
||
|
|
finally:
|
||
|
|
# Cleanup
|
||
|
|
if os.path.exists(test_db_path):
|
||
|
|
os.remove(test_db_path)
|
||
|
|
if os.path.exists(f'{original_db}.backup'):
|
||
|
|
os.rename(f'{original_db}.backup', original_db)
|
||
|
|
if 'TEST_DB' in os.environ:
|
||
|
|
del os.environ['TEST_DB']
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def clean_db():
|
||
|
|
"""Fixture that ensures we have a clean database for each test"""
|
||
|
|
# This is simpler - just create a temp database for each test
|
||
|
|
import tempfile
|
||
|
|
import shutil
|
||
|
|
|
||
|
|
# Create temp directory for database
|
||
|
|
temp_dir = tempfile.mkdtemp()
|
||
|
|
db_path = os.path.join(temp_dir, 'test.db')
|
||
|
|
|
||
|
|
# Initialize database
|
||
|
|
conn = sqlite3.connect(db_path)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute('''
|
||
|
|
CREATE TABLE IF NOT EXISTS tablets (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
brand TEXT NOT NULL,
|
||
|
|
model TEXT NOT NULL,
|
||
|
|
serial_number TEXT UNIQUE NOT NULL,
|
||
|
|
status TEXT DEFAULT 'available'
|
||
|
|
)
|
||
|
|
''')
|
||
|
|
|
||
|
|
cursor.execute('''
|
||
|
|
CREATE TABLE IF NOT EXISTS users (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
identification TEXT UNIQUE
|
||
|
|
)
|
||
|
|
''')
|
||
|
|
|
||
|
|
cursor.execute('''
|
||
|
|
CREATE TABLE IF NOT EXISTS loans (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
tablet_id INTEGER NOT NULL,
|
||
|
|
user_id INTEGER NOT NULL,
|
||
|
|
loan_date TEXT NOT NULL,
|
||
|
|
return_date TEXT,
|
||
|
|
status TEXT DEFAULT 'active',
|
||
|
|
FOREIGN KEY (tablet_id) REFERENCES tablets (id),
|
||
|
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||
|
|
)
|
||
|
|
''')
|
||
|
|
|
||
|
|
cursor.execute('''
|
||
|
|
CREATE TABLE IF NOT EXISTS non_loanable_devices (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
brand TEXT NOT NULL,
|
||
|
|
model TEXT NOT NULL,
|
||
|
|
serial_number TEXT UNIQUE NOT NULL,
|
||
|
|
device_type TEXT NOT NULL,
|
||
|
|
location TEXT,
|
||
|
|
status TEXT DEFAULT 'available',
|
||
|
|
notes TEXT,
|
||
|
|
purchase_date TEXT,
|
||
|
|
purchase_cost REAL
|
||
|
|
)
|
||
|
|
''')
|
||
|
|
|
||
|
|
conn.commit()
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
# Temporarily replace the database
|
||
|
|
original_db = 'tablets.db'
|
||
|
|
backup_path = f'{original_db}.test_backup'
|
||
|
|
|
||
|
|
# Backup original if exists
|
||
|
|
if os.path.exists(original_db):
|
||
|
|
if os.path.exists(backup_path):
|
||
|
|
os.remove(backup_path)
|
||
|
|
os.rename(original_db, backup_path)
|
||
|
|
|
||
|
|
# Copy temp db to tablets.db location
|
||
|
|
shutil.copy(db_path, original_db)
|
||
|
|
|
||
|
|
yield original_db
|
||
|
|
|
||
|
|
# Cleanup
|
||
|
|
if os.path.exists(original_db):
|
||
|
|
os.remove(original_db)
|
||
|
|
if os.path.exists(backup_path):
|
||
|
|
os.rename(backup_path, original_db)
|
||
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||
|
|
|
||
|
|
|
||
|
|
class TestMinimalAppFunctions:
|
||
|
|
"""Test the actual functions from minimal_app.py"""
|
||
|
|
|
||
|
|
def test_add_tablet_function(self, clean_db):
|
||
|
|
"""Test the add_tablet function"""
|
||
|
|
add_tablet('TestBrand', 'TestModel', 'TEST_SN_001')
|
||
|
|
|
||
|
|
# Verify it was added
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("SELECT * FROM tablets WHERE serial_number = 'TEST_SN_001'")
|
||
|
|
tablet = cursor.fetchone()
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
assert tablet is not None
|
||
|
|
assert tablet[1] == 'TestBrand' # brand is index 1
|
||
|
|
assert tablet[2] == 'TestModel' # model is index 2
|
||
|
|
assert tablet[3] == 'TEST_SN_001' # serial_number is index 3
|
||
|
|
|
||
|
|
def test_add_tablet_duplicate(self, clean_db, capsys):
|
||
|
|
"""Test that duplicate serial numbers are rejected"""
|
||
|
|
add_tablet('Brand1', 'Model1', 'DUP_SN')
|
||
|
|
add_tablet('Brand2', 'Model2', 'DUP_SN')
|
||
|
|
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'already exists' in captured.out
|
||
|
|
|
||
|
|
def test_add_user_function(self, clean_db):
|
||
|
|
"""Test the add_user function"""
|
||
|
|
add_user('Test User', 'TEST_ID_001')
|
||
|
|
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("SELECT * FROM users WHERE identification = 'TEST_ID_001'")
|
||
|
|
user = cursor.fetchone()
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
assert user is not None
|
||
|
|
assert user[1] == 'Test User' # name is index 1
|
||
|
|
assert user[2] == 'TEST_ID_001' # identification is index 2
|
||
|
|
|
||
|
|
def test_add_user_duplicate(self, clean_db, capsys):
|
||
|
|
"""Test that duplicate user identifications are rejected"""
|
||
|
|
add_user('User1', 'DUP_ID')
|
||
|
|
add_user('User2', 'DUP_ID')
|
||
|
|
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'already exists' in captured.out
|
||
|
|
|
||
|
|
def test_loan_tablet_function(self, clean_db):
|
||
|
|
"""Test the loan_tablet function"""
|
||
|
|
# Add tablet and user
|
||
|
|
add_tablet('LoanBrand', 'LoanModel', 'LOAN_SN_001')
|
||
|
|
add_user('LoanUser', 'LOAN_ID_001')
|
||
|
|
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute("SELECT id FROM tablets WHERE serial_number = 'LOAN_SN_001'")
|
||
|
|
tablet_id = cursor.fetchone()[0]
|
||
|
|
cursor.execute("SELECT id FROM users WHERE identification = 'LOAN_ID_001'")
|
||
|
|
user_id = cursor.fetchone()[0]
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
# Loan the tablet
|
||
|
|
loan_tablet(tablet_id, user_id)
|
||
|
|
|
||
|
|
# Verify loan was created and tablet status changed
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute("SELECT status FROM tablets WHERE id = ?", (tablet_id,))
|
||
|
|
status = cursor.fetchone()[0]
|
||
|
|
assert status == 'loaned'
|
||
|
|
|
||
|
|
cursor.execute("SELECT * FROM loans WHERE tablet_id = ? AND user_id = ?", (tablet_id, user_id))
|
||
|
|
loan = cursor.fetchone()
|
||
|
|
assert loan is not None
|
||
|
|
assert loan[5] == 'active' # status is index 5
|
||
|
|
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
def test_loan_already_loaned_tablet(self, clean_db, capsys):
|
||
|
|
"""Test loaning a tablet that's already loaned"""
|
||
|
|
add_tablet('Brand', 'Model', 'ALREADY_LOANED_SN')
|
||
|
|
add_user('User1', 'USER1_ID')
|
||
|
|
add_user('User2', 'USER2_ID')
|
||
|
|
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute("SELECT id FROM tablets WHERE serial_number = 'ALREADY_LOANED_SN'")
|
||
|
|
tablet_id = cursor.fetchone()[0]
|
||
|
|
cursor.execute("SELECT id FROM users WHERE identification = 'USER1_ID'")
|
||
|
|
user1_id = cursor.fetchone()[0]
|
||
|
|
cursor.execute("SELECT id FROM users WHERE identification = 'USER2_ID'")
|
||
|
|
user2_id = cursor.fetchone()[0]
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
# Loan to first user
|
||
|
|
loan_tablet(tablet_id, user1_id)
|
||
|
|
|
||
|
|
# Try to loan to second user - should fail
|
||
|
|
loan_tablet(tablet_id, user2_id)
|
||
|
|
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'not available' in captured.out
|
||
|
|
|
||
|
|
def test_return_tablet_function(self, clean_db):
|
||
|
|
"""Test the return_tablet function"""
|
||
|
|
add_tablet('ReturnBrand', 'ReturnModel', 'RETURN_SN_001')
|
||
|
|
add_user('ReturnUser', 'RETURN_ID_001')
|
||
|
|
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute("SELECT id FROM tablets WHERE serial_number = 'RETURN_SN_001'")
|
||
|
|
tablet_id = cursor.fetchone()[0]
|
||
|
|
cursor.execute("SELECT id FROM users WHERE identification = 'RETURN_ID_001'")
|
||
|
|
user_id = cursor.fetchone()[0]
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
# Loan the tablet
|
||
|
|
loan_tablet(tablet_id, user_id)
|
||
|
|
|
||
|
|
# Get loan ID
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("SELECT id FROM loans WHERE tablet_id = ?", (tablet_id,))
|
||
|
|
loan_id = cursor.fetchone()[0]
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
# Return the tablet
|
||
|
|
return_tablet(loan_id)
|
||
|
|
|
||
|
|
# Verify return
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute("SELECT status FROM tablets WHERE id = ?", (tablet_id,))
|
||
|
|
status = cursor.fetchone()[0]
|
||
|
|
assert status == 'available'
|
||
|
|
|
||
|
|
cursor.execute("SELECT status, return_date FROM loans WHERE id = ?", (loan_id,))
|
||
|
|
loan = cursor.fetchone()
|
||
|
|
assert loan[0] == 'returned'
|
||
|
|
assert loan[1] is not None # return_date should be set
|
||
|
|
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
def test_return_nonexistent_loan(self, clean_db, capsys):
|
||
|
|
"""Test returning a loan that doesn't exist"""
|
||
|
|
return_tablet(99999)
|
||
|
|
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'not found' in captured.out or 'Loan' in captured.out
|
||
|
|
|
||
|
|
def test_show_available_tablets(self, clean_db, capsys):
|
||
|
|
"""Test showing available tablets"""
|
||
|
|
add_tablet('Avail1', 'Model1', 'AVAIL_SN_001')
|
||
|
|
add_tablet('Avail2', 'Model2', 'AVAIL_SN_002')
|
||
|
|
|
||
|
|
show_available_tablets()
|
||
|
|
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'Available Tablets' in captured.out
|
||
|
|
assert 'AVAIL_SN_001' in captured.out
|
||
|
|
assert 'AVAIL_SN_002' in captured.out
|
||
|
|
|
||
|
|
def test_show_active_loans(self, clean_db, capsys):
|
||
|
|
"""Test showing active loans"""
|
||
|
|
add_tablet('LoanBrand', 'LoanModel', 'ACTIVE_LOAN_SN')
|
||
|
|
add_user('LoanUser', 'ACTIVE_LOAN_ID')
|
||
|
|
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute("SELECT id FROM tablets WHERE serial_number = 'ACTIVE_LOAN_SN'")
|
||
|
|
tablet_id = cursor.fetchone()[0]
|
||
|
|
cursor.execute("SELECT id FROM users WHERE identification = 'ACTIVE_LOAN_ID'")
|
||
|
|
user_id = cursor.fetchone()[0]
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
loan_tablet(tablet_id, user_id)
|
||
|
|
show_active_loans()
|
||
|
|
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'Active Loans' in captured.out
|
||
|
|
assert 'ACTIVE_LOAN_SN' in captured.out
|
||
|
|
|
||
|
|
def test_non_loanable_device_crud(self, clean_db, capsys):
|
||
|
|
"""Test CRUD operations for non-loanable devices"""
|
||
|
|
# Add
|
||
|
|
add_non_loanable_device('Projector', 'P100', 'PROJ_001', 'projector', 'Room A')
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'Added non-loanable device' in captured.out
|
||
|
|
|
||
|
|
# Show
|
||
|
|
show_non_loanable_devices()
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'PROJ_001' in captured.out
|
||
|
|
|
||
|
|
# Get ID for delete
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("SELECT id FROM non_loanable_devices WHERE serial_number = 'PROJ_001'")
|
||
|
|
device_id = cursor.fetchone()[0]
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
# Delete
|
||
|
|
delete_non_loanable_device(device_id)
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'deleted' in captured.out
|
||
|
|
|
||
|
|
# Verify deletion
|
||
|
|
conn = sqlite3.connect(clean_db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("SELECT * FROM non_loanable_devices WHERE serial_number = 'PROJ_001'")
|
||
|
|
device = cursor.fetchone()
|
||
|
|
conn.close()
|
||
|
|
assert device is None
|