18 KiB
FitPub Federation Testing Guide
This guide explains how to test the instance-to-instance federation functionality by running two FitPub instances locally.
Docker Compose Setup (Recommended)
The easiest way to test federation is using Docker Compose, which automatically sets up two complete FitPub instances with separate databases and proper networking.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Docker Network (fitpub-federation) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Instance 1 │ │ Instance 2 │ │
│ │ (instance1.local) │◄─────►│ (instance2.local) │ │
│ │ Port: 8080 │ │ Port: 8081 │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ PostgreSQL 1 │ │ PostgreSQL 2 │ │
│ │ (postgres1) │ │ (postgres2) │ │
│ │ Port: 5432 │ │ Port: 5433 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
▲ ▲
│ │
localhost:8080 localhost:8081
Quick Start
-
Start both instances:
docker-compose -f docker-compose.federation-test.yml up -d -
Check status:
docker-compose -f docker-compose.federation-test.yml ps -
Access the instances:
- Instance 1: http://localhost:8080
- Instance 2: http://localhost:8081
-
Follow the Test Scenarios below to verify federation functionality
-
View logs (in separate terminals):
# Instance 1 logs docker-compose -f docker-compose.federation-test.yml logs -f fitpub1 # Instance 2 logs docker-compose -f docker-compose.federation-test.yml logs -f fitpub2 -
Stop and clean up:
# Stop containers docker-compose -f docker-compose.federation-test.yml down # Stop and remove volumes (complete cleanup) docker-compose -f docker-compose.federation-test.yml down -v
Service Overview
The Docker Compose setup includes:
-
postgres1: PostgreSQL 16 with PostGIS 3.4 for Instance 1
- Database:
fitpub1 - Port: 5432 (internal), 5434 (on host)
- Database:
-
postgres2: PostgreSQL 16 with PostGIS 3.4 for Instance 2
- Database:
fitpub2 - Port: 5432 (internal), 5433 (on host)
- Database:
-
fitpub1: FitPub Instance 1
- Domain:
instance1.local:8080 - Port: 8080
- Network alias:
instance1.local
- Domain:
-
fitpub2: FitPub Instance 2
- Domain:
instance2.local:8081 - Port: 8081
- Network alias:
instance2.local
- Domain:
Docker-Specific Commands
Access database directly:
# Instance 1 database
docker exec -it fitpub-postgres1 psql -U fitpub -d fitpub1
# Instance 2 database
docker exec -it fitpub-postgres2 psql -U fitpub -d fitpub2
Inspect network:
docker network inspect fitpub-federation
View container details:
docker inspect fitpub-instance1
docker inspect fitpub-instance2
Restart a single service:
docker-compose -f docker-compose.federation-test.yml restart fitpub1
docker-compose -f docker-compose.federation-test.yml restart fitpub2
Rebuild images (after code changes):
docker-compose -f docker-compose.federation-test.yml build
docker-compose -f docker-compose.federation-test.yml up -d
Docker Troubleshooting
Container won't start:
# Check logs for errors
docker-compose -f docker-compose.federation-test.yml logs fitpub1
docker-compose -f docker-compose.federation-test.yml logs fitpub2
# Check health status
docker ps -a | grep fitpub
Database connection issues:
# Verify database is healthy
docker-compose -f docker-compose.federation-test.yml ps postgres1
docker-compose -f docker-compose.federation-test.yml ps postgres2
# Check database logs
docker-compose -f docker-compose.federation-test.yml logs postgres1
Network connectivity issues:
# Test DNS resolution from inside container
docker exec -it fitpub-instance1 ping instance2.local
docker exec -it fitpub-instance2 ping instance1.local
# Test HTTP connectivity
docker exec -it fitpub-instance1 curl http://instance2.local:8081/.well-known/webfinger
Port already in use:
# Find process using port 8080
lsof -ti:8080 | xargs kill -9
# Or use different external ports in docker-compose.yml
Volume permission issues:
# Remove all volumes and start fresh
docker-compose -f docker-compose.federation-test.yml down -v
docker-compose -f docker-compose.federation-test.yml up -d
Platform warning on Apple Silicon (M1/M2/M3 Macs):
The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)
This is expected and safe to ignore. Docker will use emulation (Rosetta 2) to run the amd64 images. Performance may be slightly slower than native ARM images, but fully functional for testing.
Manual Setup (Alternative)
If you prefer to run the instances directly without Docker, follow these instructions:
Prerequisites
- Java 17+
- Maven 3.8+
- PostgreSQL 13+ with PostGIS extension
- Two separate PostgreSQL databases
- Two different port numbers for the applications
Setup
Step 1: Create Two PostgreSQL Databases
# Connect to PostgreSQL
psql -U postgres
# Create databases
CREATE DATABASE fitpub_instance1;
CREATE DATABASE fitpub_instance2;
# Enable PostGIS extension for both databases
\c fitpub_instance1
CREATE EXTENSION IF NOT EXISTS postgis;
\c fitpub_instance2
CREATE EXTENSION IF NOT EXISTS postgis;
\q
Step 2: Prepare Application Profiles
Create two separate application configuration files:
application-instance1.yml
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/fitpub_instance1
username: postgres
password: your_password
jpa:
hibernate:
ddl-auto: validate
fitpub:
base-url: http://localhost:8080
domain: localhost:8080
logging:
level:
net.javahippie.fitpub: DEBUG
application-instance2.yml
server:
port: 8081
spring:
datasource:
url: jdbc:postgresql://localhost:5432/fitpub_instance2
username: postgres
password: your_password
jpa:
hibernate:
ddl-auto: validate
fitpub:
base-url: http://localhost:8081
domain: localhost:8081
logging:
level:
net.javahippie.fitpub: DEBUG
Step 3: Build the Application
mvn clean package -DskipTests
Running the Instances
Terminal 1: Start Instance 1
java -jar target/feditrack-1.0-SNAPSHOT.jar --spring.profiles.active=instance1
Wait for the application to start completely. You should see:
Started FitPubApplication in X.XXX seconds
Terminal 2: Start Instance 2
java -jar target/feditrack-1.0-SNAPSHOT.jar --spring.profiles.active=instance2
Test Scenarios
Test 1: User Registration
Instance 1 (http://localhost:8080)
- Navigate to http://localhost:8080/register
- Register user:
alice/alice@localhost1.test/password123 - Login
Instance 2 (http://localhost:8081)
- Navigate to http://localhost:8081/register
- Register user:
bob/bob@localhost2.test/password123 - Login
Test 2: WebFinger Discovery
From Instance 1, discover Bob on Instance 2:
curl http://localhost:8080/.well-known/webfinger?resource=acct:bob@localhost:8081
Expected response:
{
"subject": "acct:bob@localhost:8081",
"aliases": [
"http://localhost:8081/users/bob"
],
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "http://localhost:8081/users/bob"
}
]
}
From Instance 2, discover Alice on Instance 1:
curl http://localhost:8081/.well-known/webfinger?resource=acct:alice@localhost:8080
Test 3: Remote User Discovery via UI
On Instance 1 (Alice following Bob):
- Login as Alice
- Navigate to http://localhost:8080/discover
- In the "Follow Remote Users" section, enter:
bob@localhost:8081 - Click "Search"
- Verify Bob's profile appears with avatar, display name, and bio
- Click "Follow" button
- Verify notification appears: "Follow request sent to bob@localhost:8081"
Verify on Instance 2 (Bob's perspective):
- Login as Bob on http://localhost:8081
- Check notifications - you should see: "alice@localhost:8080 followed you"
- Navigate to http://localhost:8081/users/bob/followers
- Verify alice@localhost:8080 appears in followers list
Test 4: Following Relationship Check
Check via API:
# From Instance 2, check Bob's followers
curl http://localhost:8081/api/users/bob/followers | jq
# Expected: Alice should be in the list
Check via UI:
On Instance 2:
- Navigate to http://localhost:8081/users/bob
- Check "Followers" count - should be 1
- Click on "Followers" - Alice should be listed
On Instance 1:
- Navigate to http://localhost:8080/users/alice
- Check "Following" count - should be 1
- Click on "Following" - Bob should be listed
Test 5: Activity Federation
Bob uploads a workout on Instance 2:
- Login as Bob on http://localhost:8081
- Navigate to http://localhost:8081/upload
- Upload a FIT file (use test file from
src/test/resources/) - Set title: "Morning 10K Run"
- Set visibility: "Public"
- Click "Upload"
Verify on Instance 1 (Alice's federated timeline):
- Login as Alice on http://localhost:8080
- Navigate to http://localhost:8080/timeline/federated
- Verify Bob's "Morning 10K Run" activity appears with:
- Federation badge: "🌐 Remote"
- Bob's avatar and @bob@localhost:8081
- Map preview (if map image URL is available)
- Metrics (distance, duration, pace, elevation)
- Link to view on origin server
Test 6: Remote Activity Details
Click on Remote Activity:
From Alice's federated timeline:
- Click on Bob's "Morning 10K Run" activity title
- Verify it opens Bob's activity on Instance 2 (http://localhost:8081/activities/{id}) in a new tab
- Alternatively, click "View on Origin Server" button
Test 7: Incoming Activity via ActivityPub
Test with manual ActivityPub POST:
# Create a test activity
cat > test-activity.json <<EOF
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "http://localhost:8081/activities/create/test-123",
"actor": "http://localhost:8081/users/bob",
"published": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["http://localhost:8081/users/bob/followers"],
"object": {
"type": "Note",
"id": "http://localhost:8081/workouts/test-456",
"attributedTo": "http://localhost:8081/users/bob",
"name": "Test Workout via ActivityPub",
"content": "Testing direct ActivityPub activity delivery",
"published": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"workoutData": {
"activityType": "RUN",
"distance": 5000,
"duration": "PT25M30S",
"elevationGain": 50
},
"attachment": [
{
"type": "Document",
"mediaType": "image/png",
"name": "Map",
"url": "http://localhost:8081/activities/test-456/map.png"
}
]
}
}
EOF
# Post to Alice's inbox on Instance 1
curl -X POST http://localhost:8080/users/alice/inbox \
-H "Content-Type: application/activity+json" \
-d @test-activity.json
# Expected response: 202 Accepted
Verify in database:
-- On Instance 1
SELECT * FROM remote_activities WHERE remote_actor_uri = 'http://localhost:8081/users/bob';
Test 8: Unfollow Workflow
Alice unfollows Bob:
- On Instance 1, navigate to http://localhost:8080/users/alice/following
- Find Bob in the following list
- Click "Unfollow"
- Verify confirmation dialog
- Confirm unfollow
Verify Undo Activity:
Check Instance 2 logs for incoming Undo activity:
Processing Undo activity for user bob
Deleted follow from actor: http://localhost:8080/users/alice
Check Database:
-- On Instance 2
SELECT * FROM follows WHERE remote_actor_uri = 'http://localhost:8080/users/alice'
AND following_actor_uri = 'http://localhost:8081/users/bob';
-- Should return 0 rows
Test 9: Accept Activity Flow
Bob follows Alice (reverse direction):
- On Instance 2, login as Bob
- Navigate to http://localhost:8081/discover
- Search for:
alice@localhost:8080 - Click "Follow"
- Verify "Follow request sent" notification
Check Follow Status:
-- On Instance 2
SELECT status FROM follows WHERE follower_id = (SELECT id FROM users WHERE username = 'bob')
AND following_actor_uri = 'http://localhost:8080/users/alice';
-- Should return 'PENDING'
Verify Accept on Instance 1:
Check Instance 1 logs for outgoing Accept activity:
Sending Accept activity to http://localhost:8081/users/bob/inbox
Accept activity sent successfully
Check Updated Status:
-- On Instance 2
SELECT status FROM follows WHERE follower_id = (SELECT id FROM users WHERE username = 'bob')
AND following_actor_uri = 'http://localhost:8080/users/alice';
-- Should return 'ACCEPTED'
Check Notification:
On Instance 2:
- Navigate to http://localhost:8081/notifications
- Verify notification: "alice@localhost:8080 accepted your follow request"
Troubleshooting
Instance Won't Start
Problem: Port already in use
Port 8080 is already in use
Solution: Kill the process using the port or use a different port
lsof -ti:8080 | xargs kill -9
Database Connection Error
Problem: Connection refused to PostgreSQL
Solution: Check PostgreSQL is running
brew services start postgresql
# or
sudo systemctl start postgresql
WebFinger Not Working
Problem: 404 when accessing /.well-known/webfinger
Solution:
- Check if the controller is mapped correctly
- Verify Spring Security allows unauthenticated access to WebFinger endpoint
- Check logs for any errors
Remote Activities Not Appearing
Problem: Bob's activities don't show up in Alice's federated timeline
Solution:
- Verify follow relationship exists and status is ACCEPTED:
SELECT * FROM follows WHERE follower_id = (SELECT id FROM users WHERE username = 'alice') AND following_actor_uri LIKE '%bob%'; - Check InboxProcessor logs for incoming Create activities
- Verify RemoteActivity was created:
SELECT * FROM remote_activities; - Check TimelineService is fetching both local and remote activities
Map Preview Not Loading
Problem: Remote activity map shows "Map not available"
Solution:
- Remote activities use
mapImageUrlfield which must be set when creating the activity - For testing, you may need to implement map image generation on the origin server
- Check if the URL in
map_image_urlis accessible
Validation Checklist
- Both instances start successfully on different ports
- WebFinger discovery works in both directions
- Remote user discovery UI works
- Follow request is sent and creates PENDING follow
- Accept activity is received and updates status to ACCEPTED
- Follower/following lists show both local and remote users
- Remote activities appear in federated timeline
- Remote activities show federation badge
- Map preview loads from remote server
- "View on Origin Server" opens correct URL
- Unfollow sends Undo activity and removes follow
- Notifications are created for follow/accept events
- No errors in console logs
Database Inspection Queries
Check All Follows
-- On any instance
SELECT
f.id,
f.follower_id,
u.username as follower_username,
f.following_actor_uri,
f.remote_actor_uri,
f.status,
f.created_at
FROM follows f
LEFT JOIN users u ON f.follower_id = u.id
ORDER BY f.created_at DESC;
Check Remote Actors
SELECT
actor_uri,
username,
domain,
display_name,
last_fetched
FROM remote_actors
ORDER BY last_fetched DESC;
Check Remote Activities
SELECT
id,
activity_uri,
remote_actor_uri,
activity_type,
title,
total_distance,
total_duration_seconds,
published_at,
visibility,
map_image_url
FROM remote_activities
ORDER BY published_at DESC;
Clean Up
To reset the test environment:
# Stop both instances (Ctrl+C in both terminals)
# Drop and recreate databases
psql -U postgres <<EOF
DROP DATABASE fitpub_instance1;
DROP DATABASE fitpub_instance2;
CREATE DATABASE fitpub_instance1;
CREATE DATABASE fitpub_instance2;
\c fitpub_instance1
CREATE EXTENSION IF NOT EXISTS postgis;
\c fitpub_instance2
CREATE EXTENSION IF NOT EXISTS postgis;
EOF
Additional Testing Tips
- Use Browser Developer Tools to inspect network requests for ActivityPub activities
- Monitor Logs in both terminal windows to see federation events in real-time
- Test Edge Cases like following yourself, duplicate follows, following non-existent users
- Test Different Visibility Levels (PUBLIC, FOLLOWERS, PRIVATE)
- Simulate Network Failures by stopping one instance mid-federation
- Test Concurrent Operations (both users following each other simultaneously)
Success Criteria
✅ All checklist items pass ✅ No errors in logs ✅ Activities federate within 1-2 seconds ✅ UI updates reflect federation state correctly ✅ Database state is consistent across both instances