Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</head>
<body>
<h1>Hello...</h1>
<h1></h1>
<p>This is a test page for the URL watcher.</p>
<p>Last updated: <span id="timestamp"></span></p>
<script>
Expand Down
40 changes: 21 additions & 19 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/smarks/watcher)
[![Tests](https://img.shields.io/badge/tests-passing-green)](https://github.com/smarks/watcher/actions)
[![Python](https://img.shields.io/badge/python-3.9+-blue)](https://python.org)
[![AWS](https://img.shields.io/badge/aws-sns-orange)](https://aws.amazon.com/sns/)
[![Twilio](https://img.shields.io/badge/twilio-sms-red)](https://twilio.com/sms) [![AWS](https://img.shields.io/badge/aws-sns-orange)](https://aws.amazon.com/sns/)

**Perfect for beginners!** Monitor website changes, get SMS alerts, and learn Python automation. Written mostly by Claude AI under the careful direction of Spencer.

Expand All @@ -18,7 +18,7 @@ URL Watcher is like having a personal assistant that:
- 📱 **Sends you SMS alerts** when something changes
- 🔍 **Shows you exactly** what changed with before/after comparisons
- ⏰ **Works 24/7** checking sites automatically
- 🆓 **Costs almost nothing** to run (AWS SMS: ~$0.006 per message)
- 🆓 **Costs almost nothing** to run (Twilio SMS: ~$0.0075 per message, AWS SMS: ~$0.006 per message)

## 🚀 5-Minute Quick Start

Expand Down Expand Up @@ -216,37 +216,39 @@ You'll see the change detection in Terminal 2! 🎉

## 📱 SMS Notifications Setup

Get instant text message alerts when websites change!
Get instant text message alerts when websites change! Choose between Twilio (recommended) or AWS SNS.

### Option A: Quick Setup (Recommended for beginners)
### Option A: Twilio Setup (Recommended - Easier)

1. **Deploy AWS infrastructure automatically:**
```bash
cd cloudformation
./deploy.sh -p "+1234567890" # Use your real phone number
```
1. **Create a Twilio account:**
- Go to [twilio.com](https://twilio.com) and sign up (free trial available)
- Verify your phone number during signup

2. **The script will output configuration commands like:**
2. **Get your Twilio credentials:**
- In Twilio Console, find your Account SID and Auth Token
- Purchase a Twilio phone number (or use trial number)

3. **Set environment variables:**
```bash
export AWS_ACCESS_KEY_ID="AKIAEXAMPLE123"
export AWS_SECRET_ACCESS_KEY="secretkey123"
export SNS_TOPIC_ARN="arn:aws:sns:us-east-1:123456789012:url-watcher-topic"
export AWS_REGION="us-east-1"
export TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export TWILIO_AUTH_TOKEN="your_auth_token"
export TWILIO_FROM_PHONE="+15551234567" # Your Twilio number
export TWILIO_TO_PHONE="+19876543210" # Your personal number
```

3. **Copy and run those export commands, then test:**
4. **Test configuration:**
```bash
python -c "from sms_notifier import create_notifier_from_env; n=create_notifier_from_env(); print('✅ SMS working!' if n.test_notification()['success'] else '❌ SMS failed')"
python twilio_notifier.py
```

4. **Use monitoring with SMS:**
5. **Use monitoring with SMS:**
```bash
python url_watcher.py https://example.com --sms --continuous
```

### Option B: Manual AWS Setup
### Option B: AWS SNS Setup (Legacy)

If you prefer manual control or the script doesn't work:
For users who prefer AWS or already have AWS infrastructure:

1. **Create SNS Topic in AWS Console:**
- Go to AWS SNS Console
Expand Down
8 changes: 5 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
requests>=2.28.0
boto3>=1.26.0
botocore>=1.29.0
twilio>=9.0.0
pytest>=8.0.0
pytest-cov>=6.0.0
# Dev dependencies
flake8>=6.0.0
black>=23.0.0
bandit>=1.7.0
bandit>=1.7.0
# Legacy AWS dependencies (keeping for backward compatibility)
boto3>=1.26.0
botocore>=1.29.0
250 changes: 250 additions & 0 deletions test_twilio_notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""
Test suite for Twilio SMS notification functionality
"""

import os
import pytest
from unittest.mock import Mock, patch, MagicMock
from twilio_notifier import TwilioNotifier, create_notifier_from_env


class TestTwilioNotifier:
"""Test the TwilioNotifier class functionality"""

def test_init_without_credentials(self):
"""Test initializing without credentials"""
notifier = TwilioNotifier()
assert notifier.account_sid is None
assert notifier.auth_token is None
assert notifier.from_phone is None
assert notifier.to_phone is None
assert not notifier.is_configured()

@patch("twilio_notifier.TWILIO_AVAILABLE", True)
@patch("twilio_notifier.Client")
def test_init_with_credentials(self, mock_client):
"""Test initializing with credentials"""
notifier = TwilioNotifier(
account_sid="ACtest123",
auth_token="token123",
from_phone="+15551234567",
to_phone="+19876543210",
)

assert notifier.account_sid == "ACtest123"
assert notifier.auth_token == "token123"
assert notifier.from_phone == "+15551234567"
assert notifier.to_phone == "+19876543210"
mock_client.assert_called_once_with("ACtest123", "token123")

def test_is_configured_false_cases(self):
"""Test is_configured returns False for incomplete configuration"""
# No credentials
notifier = TwilioNotifier()
assert not notifier.is_configured()

# Missing phone numbers
notifier = TwilioNotifier(account_sid="test", auth_token="test")
assert not notifier.is_configured()

# Missing auth token
notifier = TwilioNotifier(
account_sid="test", from_phone="+1234567890", to_phone="+1987654321"
)
assert not notifier.is_configured()

@patch("twilio_notifier.TWILIO_AVAILABLE", True)
@patch("twilio_notifier.Client")
def test_is_configured_true(self, mock_client):
"""Test is_configured returns True for complete configuration"""
mock_client.return_value = Mock()

notifier = TwilioNotifier(
account_sid="ACtest123",
auth_token="token123",
from_phone="+15551234567",
to_phone="+19876543210",
)

assert notifier.is_configured()

@patch("twilio_notifier.TWILIO_AVAILABLE", False)
def test_is_configured_false_when_twilio_unavailable(self):
"""Test is_configured returns False when Twilio package unavailable"""
notifier = TwilioNotifier(
account_sid="ACtest123",
auth_token="token123",
from_phone="+15551234567",
to_phone="+19876543210",
)

assert not notifier.is_configured()

@patch("twilio_notifier.TWILIO_AVAILABLE", True)
@patch("twilio_notifier.Client")
def test_send_notification_success(self, mock_client):
"""Test successful SMS notification sending"""
# Setup mocks
mock_message = Mock()
mock_message.sid = "SMtest123"
mock_client.return_value.messages.create.return_value = mock_message

notifier = TwilioNotifier(
account_sid="ACtest123",
auth_token="token123",
from_phone="+15551234567",
to_phone="+19876543210",
)

# Send notification
result = notifier.send_notification(
url="https://example.com", message="Test change detected"
)

assert result is True
mock_client.return_value.messages.create.assert_called_once()

# Check the call arguments
call_args = mock_client.return_value.messages.create.call_args
assert call_args[1]["from_"] == "+15551234567"
assert call_args[1]["to"] == "+19876543210"
assert "URL CHANGE DETECTED" in call_args[1]["body"]
assert "https://example.com" in call_args[1]["body"]

def test_send_notification_not_configured(self):
"""Test send_notification fails when not configured"""
notifier = TwilioNotifier()

result = notifier.send_notification(url="https://example.com", message="Test message")

assert result is False

@patch("twilio_notifier.TWILIO_AVAILABLE", True)
@patch("twilio_notifier.Client")
def test_send_notification_twilio_exception(self, mock_client):
"""Test send_notification handles Twilio exceptions"""
# Create a mock exception that inherits from Exception
mock_exception = Exception("Test Twilio error")
mock_client.return_value.messages.create.side_effect = mock_exception

notifier = TwilioNotifier(
account_sid="ACtest123",
auth_token="token123",
from_phone="+15551234567",
to_phone="+19876543210",
)

result = notifier.send_notification(url="https://example.com", message="Test message")

assert result is False

def test_format_message(self):
"""Test message formatting"""
notifier = TwilioNotifier()

message = notifier._format_message(
url="https://example.com", content="Line changed from A to B"
)

assert "URL CHANGE DETECTED" in message
assert "https://example.com" in message
assert "Line changed from A to B" in message
assert "Powered by URL Watcher + Twilio" in message

def test_format_message_truncation(self):
"""Test message truncation for long content"""
notifier = TwilioNotifier()

# Create very long content
long_content = "A" * 1500

message = notifier._format_message(url="https://example.com", content=long_content)

# Should be truncated and have "..." at the end
assert len(message) < 1600 # Under SMS limit
assert message.count("A") < 1500 # Content was truncated
assert "..." in message

@patch("twilio_notifier.TWILIO_AVAILABLE", True)
@patch("twilio_notifier.Client")
def test_test_notification_success(self, mock_client):
"""Test successful test notification"""
mock_message = Mock()
mock_message.sid = "SMtest123"
mock_message.status = "sent"
mock_client.return_value.messages.create.return_value = mock_message

notifier = TwilioNotifier(
account_sid="ACtest123",
auth_token="token123",
from_phone="+15551234567",
to_phone="+19876543210",
)

result = notifier.test_notification()

assert result["success"] is True
assert result["message_id"] == "SMtest123"
assert result["status"] == "sent"

@patch("twilio_notifier.TWILIO_AVAILABLE", True)
@patch("twilio_notifier.Client")
def test_test_notification_not_configured(self, mock_client):
"""Test test notification when not configured"""
notifier = TwilioNotifier() # No credentials provided

result = notifier.test_notification()

assert result["success"] is False
assert "not configured" in result["message"]
assert "Missing:" in result["error"]

@patch("twilio_notifier.TWILIO_AVAILABLE", False)
def test_test_notification_twilio_unavailable(self):
"""Test test notification when Twilio package unavailable"""
notifier = TwilioNotifier()

result = notifier.test_notification()

assert result["success"] is False
assert "not installed" in result["message"]


class TestCreateNotifierFromEnv:
"""Test the create_notifier_from_env function"""

@patch("twilio_notifier.TWILIO_AVAILABLE", True)
@patch.dict(
os.environ,
{
"TWILIO_ACCOUNT_SID": "ACtest123",
"TWILIO_AUTH_TOKEN": "token123",
"TWILIO_FROM_PHONE": "+15551234567",
"TWILIO_TO_PHONE": "+19876543210",
},
)
@patch("twilio_notifier.Client")
def test_create_from_env_success(self, mock_client):
"""Test creating notifier from environment variables"""
notifier = create_notifier_from_env()

assert notifier.account_sid == "ACtest123"
assert notifier.auth_token == "token123"
assert notifier.from_phone == "+15551234567"
assert notifier.to_phone == "+19876543210"

@patch.dict(os.environ, {}, clear=True)
def test_create_from_env_no_vars(self):
"""Test creating notifier with no environment variables"""
notifier = create_notifier_from_env()

assert notifier.account_sid is None
assert notifier.auth_token is None
assert notifier.from_phone is None
assert notifier.to_phone is None
assert not notifier.is_configured()


if __name__ == "__main__":
pytest.main([__file__, "-v"])
Loading