Better Federation Support

This commit is contained in:
Tim Zöller 2025-12-15 21:55:17 +01:00
parent 15b420b87a
commit 5b687883b0
22 changed files with 2931 additions and 49 deletions

516
FEDERATION_TESTING_GUIDE.md Normal file
View file

@ -0,0 +1,516 @@
# FitPub Federation Testing Guide
This guide explains how to test the instance-to-instance federation functionality by running two FitPub instances locally.
## 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
```bash
# 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`
```yaml
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`
```yaml
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
```bash
mvn clean package -DskipTests
```
## Running the Instances
### Terminal 1: Start Instance 1
```bash
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
```bash
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:**
```bash
curl http://localhost:8080/.well-known/webfinger?resource=acct:bob@localhost:8081
```
Expected response:
```json
{
"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:**
```bash
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:**
```bash
# 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:**
```bash
# 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:**
```sql
-- 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:**
```sql
-- 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:**
```sql
-- 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:**
```sql
-- 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
```bash
lsof -ti:8080 | xargs kill -9
```
### Database Connection Error
**Problem:** Connection refused to PostgreSQL
**Solution:** Check PostgreSQL is running
```bash
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:
```sql
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:
```sql
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
```sql
-- 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
```sql
SELECT
actor_uri,
username,
domain,
display_name,
last_fetched
FROM remote_actors
ORDER BY last_fetched DESC;
```
### Check Remote Activities
```sql
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:
```bash
# 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