# ActivityPub Testing Guide for Solar Network This guide will help you test the ActivityPub implementation in Solar Network, starting with a self-hosted instance and then moving to a real instance. ## Prerequisites - ✅ Solar Network codebase with ActivityPub implementation - ✅ Docker installed (for running Mastodon/Fediverse instances) - ✅ PostgreSQL database running - ✅ `.NET 10` SDK ## Part 1: Set Up a Self-Hosted Test Instance ### Option A: Using Mastodon (Recommended for Compatibility) #### 1. Create a Docker Compose File Create `docker-compose.mastodon-test.yml`: ```yaml version: '3' services: db: restart: always image: postgres:14-alpine environment: POSTGRES_USER: mastodon POSTGRES_PASSWORD: mastodon_password POSTGRES_DB: mastodon networks: - mastodon_network healthcheck: test: ["CMD", "pg_isready", "-U", "mastodon"] interval: 5s retries: 5 redis: restart: always image: redis:7-alpine networks: - mastodon_network healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s retries: 5 es: restart: always image: docker.elastic.co/elasticsearch:8.10.2 environment: - "discovery.type=single-node" - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - "xpack.security.enabled=false" networks: - mastodon_network healthcheck: test: ["CMD-SHELL", "curl -silent http://localhost:9200/_cluster/health || exit 1"] interval: 10s retries: 10 web: restart: always image: tootsuite/mastodon:latest env_file: .env.mastodon command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" ports: - "3001:3000" depends_on: - db - redis - es networks: - mastodon_network volumes: - ./mastodon-data/public:/mastodon/public/system streaming: restart: always image: tootsuite/mastodon:latest env_file: .env.mastodon command: node ./streaming ports: - "4000:4000" depends_on: - db - redis networks: - mastodon_network sidekiq: restart: always image: tootsuite/mastodon:latest env_file: .env.mastodon command: bundle exec sidekiq depends_on: - db - redis networks: - mastodon_network networks: mastodon_network: driver: bridge ``` #### 2. Create Environment File Create `.env.mastodon`: ```bash # Federation LOCAL_DOMAIN=mastodon.local LOCAL_HTTPS=false # Database DB_HOST=db DB_PORT=5432 DB_USER=mastodon DB_NAME=mastodon DB_PASS=mastodon_password # Redis REDIS_HOST=redis REDIS_PORT=6379 # Elasticsearch ES_ENABLED=true ES_HOST=es ES_PORT=9200 # Secrets (generate these!) SECRET_KEY_BASE=change_me_to_a_random_string_at_least_32_chars OTP_SECRET=change_me_to_another_random_string # Defaults SINGLE_USER_MODE=false DEFAULT_LOCALE=en ``` **Generate secrets:** ```bash # Run these to generate random secrets openssl rand -base64 32 ``` #### 3. Start Mastodon ```bash docker-compose -f docker-compose.mastodon-test.yml up -d # Check logs docker-compose -f docker-compose.mastodon-test.yml logs -f web ``` Wait for the web service to be healthy (may take 2-5 minutes). #### 4. Create a Mastodon Account ```bash # Run this command to create an admin account docker-compose -f docker-compose.mastodon-test.yml exec web \ bin/tootctl accounts create \ testuser \ testuser@mastodon.local \ --email=test@example.com \ --confirmed \ --role=admin \ --approve ``` Set password: `TestPassword123!` #### 5. Update Your /etc/hosts ```bash sudo nano /etc/hosts ``` Add: ``` 127.0.0.1 mastodon.local 127.0.0.1 solar.local ``` ### Option B: Using GoToSocial (Lightweight Alternative) Create `docker-compose.gotosocial.yml`: ```yaml version: '3' services: gotosocial: image: superseriousbusiness/gotosocial:latest environment: - GTS_HOST=gotosocial.local - GTS_ACCOUNT_DOMAIN=gotosocial.local - GTS_PROTOCOL=http - GTS_DB_TYPE=sqlite - GTS_DB_ADDRESS=/gotosocial/data/sqlite.db - GTS_STORAGE_LOCAL_BASE_PATH=/gotosocial/data/storage ports: - "3002:8080" volumes: - ./gotosocial-data:/gotosocial/data networks: default: ``` Start it: ```bash docker-compose -f docker-compose.gotosocial.yml up -d ``` Create account: ```bash docker-compose -f docker-compose.gotosocial.yml exec gotosocial \ /gotosocial/gotosocial admin account create \ --username testuser \ --email test@example.com \ --password TestPassword123! ``` ## Part 2: Configure Solar Network for Federation ### 1. Update appsettings.json Edit `DysonNetwork.Sphere/appsettings.json`: ```json { "ActivityPub": { "Domain": "solar.local", "EnableFederation": true }, "Kestrel": { "Endpoints": { "Http": { "Url": "http://solar.local:5000" } } } } ``` ### 2. Update /etc/hosts Add both instances: ``` 127.0.0.1 mastodon.local 127.0.0.1 solar.local 127.0.0.1 gotosocial.local ``` ### 3. Apply Database Migrations ```bash cd DysonNetwork.Sphere dotnet ef database update ``` ### 4. Start Solar Network ```bash dotnet run --project DysonNetwork.Sphere ``` Solar Network should now be running on `http://solar.local:5000` ## Part 3: Create Test Users ### In Solar Network 1. Open http://solar.local:5000 (or your web interface) 2. Create a new account/publisher named `solaruser` 3. Note down the publisher ID for later **Or via API** (if you have an existing account): ```bash # First, create a publisher in Solar Network curl -X POST http://solar.local:5000/api/publishers \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "name": "solaruser", "nick": "Solar User", "bio": "Testing ActivityPub federation!", "type": 0 }' ``` ### In Mastodon Open http://mastodon.local:3001 and log in with: - Username: `testuser` - Password: `TestPassword123!` ## Part 4: Test Federation Scenarios ### Test 1: WebFinger Discovery **Goal**: Verify Solar Network is discoverable ```bash # Query Solar Network's WebFinger endpoint curl -v "http://solar.local:5000/.well-known/webfinger?resource=acct:solaruser@solar.local" # Expected response (200 OK): { "subject": "acct:solaruser@solar.local", "links": [ { "rel": "self", "type": "application/activity+json", "href": "https://solar.local:5000/activitypub/actors/solaruser" }, { "rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://solar.local:5000/users/solaruser" } ] } ``` ### Test 2: Fetch Actor Profile **Goal**: Get ActivityPub actor JSON ```bash # Fetch Solar Network actor from Mastodon curl -H "Accept: application/activity+json" \ http://solar.local:5000/activitypub/actors/solaruser # Expected response includes: { "@context": ["https://www.w3.org/ns/activitystreams"], "id": "https://solar.local:5000/activitypub/actors/solaruser", "type": "Person", "preferredUsername": "solaruser", "inbox": "https://solar.local:5000/activitypub/actors/solaruser/inbox", "outbox": "https://solar.local:5000/activitypub/actors/solaruser/outbox", "followers": "https://solar.local:5000/activitypub/actors/solaruser/followers", "publicKey": { "id": "https://solar.local:5000/activitypub/actors/solaruser#main-key", "owner": "https://solar.local:5000/activitypub/actors/solaruser", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n..." } } ``` ### Test 3: Follow from Mastodon to Solar Network **Goal**: Mastodon user follows Solar Network user 1. **In Mastodon**: - Go to http://mastodon.local:3001 - In search bar, type: `@solaruser@solar.local` - Click the follow button 2. **Verify in Solar Network**: ```bash # Check database for relationship psql -d dyson_network -c \ "SELECT * FROM fediverse_relationships WHERE is_local_actor = true;" ``` 3. **Check Solar Network logs**: Should see: ``` Processing activity type: Follow from actor: ... Processed follow from ... to ... ``` 4. **Verify Mastodon receives Accept**: - Check Mastodon logs for Accept activity - Verify follow appears as accepted in Mastodon ### Test 4: Follow from Solar Network to Mastodon **Goal**: Solar Network user follows Mastodon user You'll need to call the ActivityPub delivery service: ```bash # Via API (you'll need to implement this endpoint): curl -X POST http://solar.local:5000/api/activitypub/follow \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "targetActorUri": "http://mastodon.local:3001/users/testuser" }' ``` **Or test directly with curl** (simulating a Follow activity): ```bash # Create a Follow activity curl -X POST http://solar.local:5000/activitypub/actors/solaruser/inbox \ -H "Content-Type: application/activity+json" \ -d '{ "@context": "https://www.w3.org/ns/activitystreams", "id": "http://mastodon.local:3001/test-follow-activity", "type": "Follow", "actor": "http://mastodon.local:3001/users/testuser", "object": "https://solar.local:5000/activitypub/actors/solaruser" }' ``` ### Test 5: Create a Post in Solar Network **Goal**: Post federates to Mastodon 1. **Create a post via Solar Network API**: ```bash curl -X POST http://solar.local:5000/api/posts \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "content": "Hello fediverse! Testing ActivityPub from Solar Network! 🚀", "visibility": 0, "publisherId": "PUBLISHER_ID" }' ``` 2. **Wait a few seconds** 3. **Check in Mastodon**: - Go to http://mastodon.local:3001 - The post should appear in the federated timeline - It should show `@solaruser@solar.local` as the author 4. **Verify Solar Network logs**: ``` Successfully sent activity to http://mastodon.local:3001/inbox ``` ### Test 6: Like from Mastodon **Goal**: Mastodon user likes a Solar Network post 1. **In Mastodon**: - Find the Solar Network post - Click the favorite/like button 2. **Verify in Solar Network**: ```bash psql -d dyson_network -c \ "SELECT * FROM fediverse_reactions;" ``` 3. **Check Solar Network logs**: ``` Processing activity type: Like from actor: ... Processed like from ... ``` ### Test 7: Reply from Mastodon **Goal**: Reply federates to Solar Network 1. **In Mastodon**: - Reply to the Solar Network post - Write: "@solaruser Nice to meet you!" 2. **Verify in Solar Network**: ```bash psql -d dyson_network -c \ "SELECT * FROM fediverse_contents WHERE in_reply_to IS NOT NULL;" ``` ## Part 5: Debugging and Troubleshooting ### Enable Detailed Logging Edit `appsettings.json`: ```json { "Logging": { "LogLevel": { "Default": "Debug", "DysonNetwork.Sphere.ActivityPub": "Trace" } } } ``` ### Check Database State ```bash # Check actors psql -d dyson_network -c \ "SELECT uri, username, display_name FROM fediverse_actors;" # Check contents psql -d dyson_network -c \ "SELECT uri, type, content FROM fediverse_contents;" # Check relationships psql -d dyson_network -c \ "SELECT * FROM fediverse_relationships;" # Check activities psql -d dyson_network -c \ "SELECT type, status, error_message FROM fediverse_activities;" # Check failed activities psql -d dyson_network -c \ "SELECT * FROM fediverse_activities WHERE status = 3;" # 3 = Failed ``` ### Common Issues #### Issue: "Failed to verify signature" **Cause**: HTTP Signature verification failed **Solutions**: 1. Check the signature header format 2. Verify public key matches actor's keyId 3. Ensure Date header is within 5 minutes 4. Check host header matches request URL #### Issue: "Target actor or inbox not found" **Cause**: Remote actor not fetched yet **Solutions**: 1. Manually fetch the actor first 2. Check actor URL is correct 3. Verify remote instance is accessible #### Issue: "Content already exists" **Cause**: Duplicate activity received **Solutions**: 1. This is normal - deduplication is working 2. Check if content appears correctly #### Issue: CORS errors when testing from browser **Cause**: Browser blocking cross-origin requests **Solutions**: 1. Use curl for API testing 2. Or disable CORS in development 3. Test directly from Mastodon interface ### View HTTP Signatures For debugging, you can inspect the signature: ```bash # From Mastodon to Solar Network curl -v -X POST http://solar.local:5000/activitypub/actors/solaruser/inbox \ -H "Content-Type: application/activity+json" \ -d '{"type":"Follow",...}' ``` Look for the `Signature` header in the output. ### Test HTTP Signature Verification Manually Create a test script `test-signature.js`: ```javascript const crypto = require('crypto'); // Test signature verification const publicKey = `-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----`; const signingString = `(request-target): post /activitypub/actors/solaruser/inbox host: solar.local:5000 date: ${new Date().toUTCString()} content-length: ...`; const signature = '...'; const verify = crypto.createVerify('SHA256'); verify.update(signingString); const isValid = verify.verify(publicKey, signature, 'base64'); console.log('Signature valid:', isValid); ``` ## Part 6: Test with a Real Instance ### Preparing for Public Federation 1. **Get a real domain** (e.g., via ngrok or a VPS) ```bash # Using ngrok for testing ngrok http 5000 # This gives you: https://random-id.ngrok-free.app ``` 2. **Update Solar Network config**: ```json { "ActivityPub": { "Domain": "your-domain.com", "EnableFederation": true } } ``` 3. **Update DNS** (if using real domain): - Add A record pointing to your server - Configure HTTPS (required for production federation) 4. **Test WebFinger with your domain**: ```bash curl "https://your-domain.com/.well-known/webfinger?resource=acct:username@your-domain.com" ``` ### Test with Mastodon.social 1. **Create a Mastodon.social account** - Go to https://mastodon.social - Sign up for a test account 2. **Search for your Solar Network user**: - In Mastodon.social search: `@username@your-domain.com` - Click follow 3. **Create a post in Solar Network** - Should appear in Mastodon.social 4. **Reply from Mastodon.social** - Should appear in Solar Network ### Test with Other Instances - **Pleroma**: Similar to Mastodon, good for testing - **Lemmy**: For testing community features (later) - **Pixelfed**: For testing media posts - **PeerTube**: For testing video content (later) ## Part 7: Verification Checklist ### Self-Hosted Instance Tests - [ ] WebFinger returns correct actor links - [ ] Actor profile has all required fields - [ ] Follow from Mastodon to Solar Network works - [ ] Follow from Solar Network to Mastodon works - [ ] Accept activity sent back to Mastodon - [ ] Posts from Solar Network appear in Mastodon timeline - [ ] Posts from Mastodon appear in Solar Network database - [ ] Likes from Mastodon appear in Solar Network - [ ] Replies from Mastodon appear in Solar Network - [ ] Keys are properly generated and stored - [ ] HTTP signatures are correctly verified - [ ] Outbox returns public posts ### Real Instance Tests - [ ] Domain is publicly accessible - [ ] HTTPS is working (or HTTP for local testing) - [ ] WebFinger works with your domain - [ ] Actor is discoverable from other instances - [ ] Posts federate to public instances - [ ] Users can follow across instances - [ ] Timelines show federated content ## Part 8: Monitoring During Tests ### Check Solar Network Logs ```bash # Follow logs in real-time dotnet run --project DysonNetwork.Sphere | grep -i activitypub ``` ### Check Mastodon Logs ```bash docker-compose -f docker-compose.mastodon-test.yml logs -f web | grep -i federation ``` ### Monitor Database Activity ```bash # Watch activity table watch -n 2 'psql -d dyson_network -c "SELECT type, status, created_at FROM fediverse_activities ORDER BY created_at DESC LIMIT 10;"' ``` ### Check Network Traffic ```bash # Monitor HTTP requests tcpdump -i lo port 5000 or port 3001 -A ``` ## Part 9: Advanced Testing ### Test HTTP Signature Fallbacks Test with various signature headers: ```bash # With Date header curl -H "Date: $(date -u +%a,\ %d\ %b\ %Y\ %T\ GMT)" ... # With Digest header curl -H "Digest: SHA-256=$(echo -n '{}' | openssl dgst -sha256 -binary | base64)" ... # Multiple signed headers curl -H "Signature: keyId=\"...\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"...\"" ... ``` ### Test Rate Limiting Send multiple requests quickly: ```bash for i in {1..10}; do curl -X POST http://solar.local:5000/activitypub/actors/solaruser/inbox \ -H "Content-Type: application/activity+json" \ -d '{"type":"Create",...}' done ``` ### Test Large Posts Send post with attachments: ```bash curl -X POST http://solar.local:5000/api/posts \ -H "Content-Type: application/json" \ -d '{ "content": "A post with an image", "attachments": [{"id": "file-id"}], "visibility": 0 }' ``` ## Part 10: Cleanup ### Stop Test Instances ```bash # Stop Mastodon docker-compose -f docker-compose.mastodon-test.yml down # Stop GoToSocial docker-compose -f docker-compose.gotosocial.yml down # Remove data volumes docker-compose -f docker-compose.mastodon-test.yml down -v ``` ### Reset Solar Network Database ```bash # Warning: This deletes all data! cd DysonNetwork.Sphere dotnet ef database drop dotnet ef database update ``` ### Remove /etc/hosts Entries ```bash sudo nano /etc/hosts # Remove these lines: # 127.0.0.1 mastodon.local # 127.0.0.1 solar.local # 127.0.0.1 gotosocial.local ``` ## Next Steps After Testing 1. **Fix any issues found during testing** 2. **Add retry logic for failed deliveries** 3. **Implement activity queue for async processing** 4. **Add monitoring and metrics** 5. **Test with more instances (Pleroma, Pixelfed, etc.)** 6. **Add support for more activity types** 7. **Improve error handling and logging** 8. **Add admin interface for managing federation** ## Useful Tools ### ActivityPub Testing Tools - [ActivityPub Playground](https://swicth.github.io/activity-pub-playground/) - [FediTest](https://feditest.com/) - [FediVerse.net](https://fedi.net/) ### HTTP Testing - [curl](https://curl.se/) - [httpie](https://httpie.io/) - [Postman](https://www.postman.com/) ### JSON Inspection - [jq](https://stedolan.github.io/jq/) - [jsonpath.com](https://jsonpath.com/) ### Network Debugging - [Wireshark](https://www.wireshark.org/) - [tcpdump](https://www.tcpdump.org/) ## References - [ActivityPub W3C Spec](https://www.w3.org/TR/activitypub/) - [HTTP Signatures Draft](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) - [WebFinger RFC 7033](https://tools.ietf.org/html/rfc7033) - [Mastodon Federation Documentation](https://docs.joinmastodon.org/admin/federation/)