fitpub/FEDERATION_TESTING_GUIDE.md
2026-01-01 23:48:05 +01:00

18 KiB

FitPub Federation Testing Guide

This guide explains how to test the instance-to-instance federation functionality by running two FitPub instances locally.

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

  1. Start both instances:

    docker-compose -f docker-compose.federation-test.yml up -d
    
  2. Check status:

    docker-compose -f docker-compose.federation-test.yml ps
    
  3. Access the instances:

  4. Follow the Test Scenarios below to verify federation functionality

  5. 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
    
  6. 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)
  • postgres2: PostgreSQL 16 with PostGIS 3.4 for Instance 2

    • Database: fitpub2
    • Port: 5432 (internal), 5433 (on host)
  • fitpub1: FitPub Instance 1

    • Domain: instance1.local:8080
    • Port: 8080
    • Network alias: instance1.local
  • fitpub2: FitPub Instance 2

    • Domain: instance2.local:8081
    • Port: 8081
    • Network alias: instance2.local

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:
    org.operaton.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:
    org.operaton.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)

  1. Navigate to http://localhost:8080/register
  2. Register user: alice / alice@localhost1.test / password123
  3. Login

Instance 2 (http://localhost:8081)

  1. Navigate to http://localhost:8081/register
  2. Register user: bob / bob@localhost2.test / password123
  3. 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):

  1. Login as Alice
  2. Navigate to http://localhost:8080/discover
  3. In the "Follow Remote Users" section, enter: bob@localhost:8081
  4. Click "Search"
  5. Verify Bob's profile appears with avatar, display name, and bio
  6. Click "Follow" button
  7. Verify notification appears: "Follow request sent to bob@localhost:8081"

Verify on Instance 2 (Bob's perspective):

  1. Login as Bob on http://localhost:8081
  2. Check notifications - you should see: "alice@localhost:8080 followed you"
  3. Navigate to http://localhost:8081/users/bob/followers
  4. 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:

  1. Navigate to http://localhost:8081/users/bob
  2. Check "Followers" count - should be 1
  3. Click on "Followers" - Alice should be listed

On Instance 1:

  1. Navigate to http://localhost:8080/users/alice
  2. Check "Following" count - should be 1
  3. Click on "Following" - Bob should be listed

Test 5: Activity Federation

Bob uploads a workout on Instance 2:

  1. Login as Bob on http://localhost:8081
  2. Navigate to http://localhost:8081/upload
  3. Upload a FIT file (use test file from src/test/resources/)
  4. Set title: "Morning 10K Run"
  5. Set visibility: "Public"
  6. Click "Upload"

Verify on Instance 1 (Alice's federated timeline):

  1. Login as Alice on http://localhost:8080
  2. Navigate to http://localhost:8080/timeline/federated
  3. 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:

  1. Click on Bob's "Morning 10K Run" activity title
  2. Verify it opens Bob's activity on Instance 2 (http://localhost:8081/activities/{id}) in a new tab
  3. 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:

  1. On Instance 1, navigate to http://localhost:8080/users/alice/following
  2. Find Bob in the following list
  3. Click "Unfollow"
  4. Verify confirmation dialog
  5. 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):

  1. On Instance 2, login as Bob
  2. Navigate to http://localhost:8081/discover
  3. Search for: alice@localhost:8080
  4. Click "Follow"
  5. 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:

  1. Navigate to http://localhost:8081/notifications
  2. 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:

  1. Check if the controller is mapped correctly
  2. Verify Spring Security allows unauthenticated access to WebFinger endpoint
  3. Check logs for any errors

Remote Activities Not Appearing

Problem: Bob's activities don't show up in Alice's federated timeline

Solution:

  1. 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%';
    
  2. Check InboxProcessor logs for incoming Create activities
  3. Verify RemoteActivity was created:
    SELECT * FROM remote_activities;
    
  4. 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 mapImageUrl field 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_url is 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

  1. Use Browser Developer Tools to inspect network requests for ActivityPub activities
  2. Monitor Logs in both terminal windows to see federation events in real-time
  3. Test Edge Cases like following yourself, duplicate follows, following non-existent users
  4. Test Different Visibility Levels (PUBLIC, FOLLOWERS, PRIVATE)
  5. Simulate Network Failures by stopping one instance mid-federation
  6. 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