diff --git a/docker/federation-test/run-tests.sh b/docker/federation-test/run-tests.sh index 3848414b9..f166d4903 100755 --- a/docker/federation-test/run-tests.sh +++ b/docker/federation-test/run-tests.sh @@ -13,8 +13,8 @@ pushd ../../ui yarn popd -mkdir -p volumes/pictrs_{alpha,beta,gamma} -sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma} +mkdir -p volumes/pictrs_{alpha,beta,gamma,delta,epsilon} +sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma,delta,epsilon} sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest @@ -28,6 +28,8 @@ echo "Waiting for Lemmy to start..." while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8570/api/v1/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8580/api/v1/site')" != "200" ]]; do sleep 1; done yarn api-test || true popd diff --git a/docker/federation-test/servers.sh b/docker/federation-test/servers.sh index b34e8c4ef..5b09bc952 100755 --- a/docker/federation-test/servers.sh +++ b/docker/federation-test/servers.sh @@ -12,8 +12,8 @@ pushd ../../ui yarn popd -mkdir -p volumes/pictrs_{alpha,beta,gamma} -sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma} +mkdir -p volumes/pictrs_{alpha,beta,gamma,delta,epsilon} +sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma,delta,epsilon} sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest diff --git a/docker/federation-test/tests.sh b/docker/federation-test/tests.sh index 2e88ffb25..58472e95c 100755 --- a/docker/federation-test/tests.sh +++ b/docker/federation-test/tests.sh @@ -6,5 +6,7 @@ echo "Waiting for Lemmy to start..." while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8570/api/v1/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8580/api/v1/site')" != "200" ]]; do sleep 1; done yarn api-test || true popd diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index a3d0cf431..32fee74ab 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -7,16 +7,20 @@ services: - "8540:8540" - "8550:8550" - "8560:8560" + - "8570:8570" + - "8580:8580" volumes: # Hack to make this work from both docker/federation/ and docker/federation-test/ - ../federation/nginx.conf:/etc/nginx/nginx.conf restart: on-failure depends_on: - - lemmy-alpha - pictrs + - iframely + - lemmy-alpha - lemmy-beta - lemmy-gamma - - iframely + - lemmy-delta + - lemmy-epsilon pictrs: restart: always @@ -34,7 +38,7 @@ services: - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__TLS_ENABLED=false - - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma + - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma,lemmy-delta,lemmy-epsilon - LEMMY_PORT=8540 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha - LEMMY_SETUP__ADMIN_PASSWORD=lemmy @@ -64,7 +68,7 @@ services: - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__TLS_ENABLED=false - - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma + - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma,lemmy-delta,lemmy-epsilon - LEMMY_PORT=8550 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta - LEMMY_SETUP__ADMIN_PASSWORD=lemmy @@ -94,7 +98,7 @@ services: - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__TLS_ENABLED=false - - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta + - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta,lemmy-delta,lemmy-epsilon - LEMMY_PORT=8560 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma - LEMMY_SETUP__ADMIN_PASSWORD=lemmy @@ -115,6 +119,68 @@ services: volumes: - ./volumes/postgres_gamma:/var/lib/postgresql/data + # An instance with only an allowlist for beta + lemmy-delta: + image: lemmy-federation:latest + environment: + - LEMMY_HOSTNAME=lemmy-delta:8570 + - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_delta:5432/lemmy + - LEMMY_JWT_SECRET=changeme + - LEMMY_FRONT_END_DIR=/app/dist + - LEMMY_FEDERATION__ENABLED=true + - LEMMY_FEDERATION__TLS_ENABLED=false + - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta + - LEMMY_PORT=8570 + - LEMMY_SETUP__ADMIN_USERNAME=lemmy_delta + - LEMMY_SETUP__ADMIN_PASSWORD=lemmy + - LEMMY_SETUP__SITE_NAME=lemmy-delta + - LEMMY_RATE_LIMIT__POST=99999 + - LEMMY_RATE_LIMIT__REGISTER=99999 + - LEMMY_CAPTCHA__ENABLED=false + - RUST_BACKTRACE=1 + - RUST_LOG=debug + depends_on: + - postgres_delta + postgres_delta: + image: postgres:12-alpine + environment: + - POSTGRES_USER=lemmy + - POSTGRES_PASSWORD=password + - POSTGRES_DB=lemmy + volumes: + - ./volumes/postgres_delta:/var/lib/postgresql/data + + # An instance who has a blocklist, with lemmy-alpha blocked + lemmy-epsilon: + image: lemmy-federation:latest + environment: + - LEMMY_HOSTNAME=lemmy-epsilon:8580 + - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_epsilon:5432/lemmy + - LEMMY_JWT_SECRET=changeme + - LEMMY_FRONT_END_DIR=/app/dist + - LEMMY_FEDERATION__ENABLED=true + - LEMMY_FEDERATION__TLS_ENABLED=false + - LEMMY_FEDERATION__BLOCKED_INSTANCES=lemmy-alpha + - LEMMY_PORT=8580 + - LEMMY_SETUP__ADMIN_USERNAME=lemmy_epsilon + - LEMMY_SETUP__ADMIN_PASSWORD=lemmy + - LEMMY_SETUP__SITE_NAME=lemmy-epsilon + - LEMMY_RATE_LIMIT__POST=99999 + - LEMMY_RATE_LIMIT__REGISTER=99999 + - LEMMY_CAPTCHA__ENABLED=false + - RUST_BACKTRACE=1 + - RUST_LOG=debug + depends_on: + - postgres_epsilon + postgres_epsilon: + image: postgres:12-alpine + environment: + - POSTGRES_USER=lemmy + - POSTGRES_PASSWORD=password + - POSTGRES_DB=lemmy + volumes: + - ./volumes/postgres_epsilon:/var/lib/postgresql/data + iframely: image: dogbin/iframely:latest volumes: diff --git a/docker/federation/nginx.conf b/docker/federation/nginx.conf index b7901c19c..6d062f70b 100644 --- a/docker/federation/nginx.conf +++ b/docker/federation/nginx.conf @@ -95,4 +95,66 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } + + server { + listen 8570; + server_name 127.0.0.1; + access_log off; + + # Upload limit for pictshare + client_max_body_size 50M; + + location / { + proxy_pass http://lemmy-delta:8570; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Cuts off the trailing slash on URLs to make them valid + rewrite ^(.+)/+$ $1 permanent; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location /iframely/ { + proxy_pass http://iframely:80/; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } + + server { + listen 8580; + server_name 127.0.0.1; + access_log off; + + # Upload limit for pictshare + client_max_body_size 50M; + + location / { + proxy_pass http://lemmy-epsilon:8580; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Cuts off the trailing slash on URLs to make them valid + rewrite ^(.+)/+$ $1 permanent; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location /iframely/ { + proxy_pass http://iframely:80/; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } } diff --git a/docker/federation/run-federation-test.bash b/docker/federation/run-federation-test.bash index 77cc981f4..0fe03aa17 100755 --- a/docker/federation/run-federation-test.bash +++ b/docker/federation/run-federation-test.bash @@ -20,7 +20,7 @@ popd || exit sudo docker build ../../ --file Dockerfile -t lemmy-federation:latest -for Item in alpha beta gamma ; do +for Item in alpha beta gamma delta epsilon ; do sudo mkdir -p volumes/pictrs_$Item sudo chown -R 991:991 volumes/pictrs_$Item done diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 63f53a4d7..1e7028c44 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -151,7 +151,10 @@ impl Perform for CreateComment { // Check for a community ban let post_id = data.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + let post = match blocking(context.pool(), move |conn| Post::read(conn, post_id)).await? { + Ok(post) => post, + Err(_e) => return Err(APIError::err("couldnt_find_post").into()), + }; check_community_ban(user.id, post.community_id, context.pool()).await?; @@ -283,7 +286,10 @@ impl Perform for EditComment { // Do the mentions / recipients let post_id = orig_comment.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + let post = match blocking(context.pool(), move |conn| Post::read(conn, post_id)).await? { + Ok(post) => post, + Err(_e) => return Err(APIError::err("couldnt_find_post").into()), + }; let updated_comment_content = updated_comment.content.to_owned(); let mentions = scrape_text_for_mentions(&updated_comment_content); @@ -379,7 +385,10 @@ impl Perform for DeleteComment { // Build the recipients let post_id = comment_view.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + let post = match blocking(context.pool(), move |conn| Post::read(conn, post_id)).await? { + Ok(post) => post, + Err(_e) => return Err(APIError::err("couldnt_find_post").into()), + }; let mentions = vec![]; let recipient_ids = send_local_notifs( mentions, @@ -476,7 +485,10 @@ impl Perform for RemoveComment { // Build the recipients let post_id = comment_view.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + let post = match blocking(context.pool(), move |conn| Post::read(conn, post_id)).await? { + Ok(post) => post, + Err(_e) => return Err(APIError::err("couldnt_find_post").into()), + }; let mentions = vec![]; let recipient_ids = send_local_notifs( mentions, @@ -655,7 +667,10 @@ impl Perform for CreateCommentLike { .await??; let post_id = orig_comment.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + let post = match blocking(context.pool(), move |conn| Post::read(conn, post_id)).await? { + Ok(post) => post, + Err(_e) => return Err(APIError::err("couldnt_find_post").into()), + }; check_community_ban(user.id, post.community_id, context.pool()).await?; let comment_id = data.comment_id; diff --git a/ui/src/api_tests/comment.spec.ts b/ui/src/api_tests/comment.spec.ts index 02cc683c4..24aca55f9 100644 --- a/ui/src/api_tests/comment.spec.ts +++ b/ui/src/api_tests/comment.spec.ts @@ -57,6 +57,11 @@ test('Create a comment', async () => { expect(betaComment.score).toBe(1); }); +test('Create a comment in a non-existent post', async () => { + let commentRes = await createComment(alpha, -1); + expect(commentRes).toStrictEqual({ error: 'couldnt_find_post' }); +}); + test('Update a comment', async () => { let commentRes = await createComment(alpha, postRes.post.id); let updateCommentRes = await updateComment(alpha, commentRes.comment.id); diff --git a/ui/src/api_tests/post.spec.ts b/ui/src/api_tests/post.spec.ts index 5da72e5a1..ab9c63fb1 100644 --- a/ui/src/api_tests/post.spec.ts +++ b/ui/src/api_tests/post.spec.ts @@ -2,6 +2,8 @@ import { alpha, beta, gamma, + delta, + epsilon, setupLogins, createPost, updatePost, @@ -22,11 +24,15 @@ beforeAll(async () => { await setupLogins(); await followBeta(alpha); await followBeta(gamma); + await followBeta(delta); + await followBeta(epsilon); }); afterAll(async () => { await unfollowRemotes(alpha); await unfollowRemotes(gamma); + await unfollowRemotes(delta); + await unfollowRemotes(epsilon); }); test('Create a post', async () => { @@ -45,6 +51,19 @@ test('Create a post', async () => { expect(betaPost.community_local).toBe(true); expect(betaPost.creator_local).toBe(false); expect(betaPost.score).toBe(1); + + // Delta only follows beta, so it should not see an alpha ap_id + let searchDelta = await searchPost(delta, postRes.post); + expect(searchDelta.posts[0]).toBeUndefined(); + + // Epsilon has alpha blocked, it should not see the alpha post + let searchEpsilon = await searchPost(epsilon, postRes.post); + expect(searchEpsilon.posts[0]).toBeUndefined(); +}); + +test('Create a post in a non-existent community', async () => { + let postRes = await createPost(alpha, -2); + expect(postRes).toStrictEqual({ error: 'couldnt_create_post' }); }); test('Unlike a post', async () => { @@ -53,6 +72,10 @@ test('Unlike a post', async () => { let unlike = await likePost(alpha, 0, postRes.post); expect(unlike.post.score).toBe(0); + // Try to unlike it again, make sure it stays at 0 + let unlike2 = await likePost(alpha, 0, postRes.post); + expect(unlike2.post.score).toBe(0); + // Make sure that post is unliked on beta let searchBeta = await searchPost(beta, postRes.post); let betaPost = searchBeta.posts[0]; @@ -67,10 +90,22 @@ test('Update a post', async () => { let search = await searchForBetaCommunity(alpha); let postRes = await createPost(alpha, search.communities[0].id); + let updatedName = 'A jest test federated post, updated'; let updatedPost = await updatePost(alpha, postRes.post); - expect(updatedPost.post.name).toBe('A jest test federated post, updated'); + expect(updatedPost.post.name).toBe(updatedName); expect(updatedPost.post.community_local).toBe(false); expect(updatedPost.post.creator_local).toBe(true); + + // Make sure that post is updated on beta + let searchBeta = await searchPost(beta, postRes.post); + let betaPost = searchBeta.posts[0]; + expect(betaPost.community_local).toBe(true); + expect(betaPost.creator_local).toBe(false); + expect(betaPost.name).toBe(updatedName); + + // Make sure lemmy beta cannot update the post + let updatedPostBeta = await updatePost(beta, betaPost); + expect(updatedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' }); }); test('Sticky a post', async () => { @@ -97,6 +132,15 @@ test('Sticky a post', async () => { expect(betaPost2.community_local).toBe(true); expect(betaPost2.creator_local).toBe(false); expect(betaPost2.stickied).toBe(false); + + // Make sure that gamma cannot sticky the post on beta + let searchGamma = await searchPost(gamma, postRes.post); + let gammaPost = searchGamma.posts[0]; + let gammaTrySticky = await stickyPost(gamma, true, gammaPost); + let searchBeta3 = await searchPost(beta, postRes.post); + let betaPost3 = searchBeta3.posts[0]; + expect(gammaTrySticky.post.stickied).toBe(true); + expect(betaPost3.stickied).toBe(false); }); test('Lock a post', async () => { @@ -152,6 +196,10 @@ test('Delete a post', async () => { // Make sure lemmy beta sees post is undeleted let betaPost2 = await getPost(beta, createFakeBetaPostToGetId); expect(betaPost2.post.deleted).toBe(false); + + // Make sure lemmy beta cannot delete the post + let deletedPostBeta = await deletePost(beta, true, betaPost2.post); + expect(deletedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' }); }); test('Remove a post from admin and community on different instance', async () => { diff --git a/ui/src/api_tests/shared.ts b/ui/src/api_tests/shared.ts index 31530ef7e..29aaff25f 100644 --- a/ui/src/api_tests/shared.ts +++ b/ui/src/api_tests/shared.ts @@ -59,50 +59,96 @@ export let gamma: API = { url: 'http://localhost:8560', }; +export let delta: API = { + url: 'http://localhost:8570', +}; + +export let epsilon: API = { + url: 'http://localhost:8580', +}; + export async function setupLogins() { - let form: LoginForm = { + let formAlpha: LoginForm = { username_or_email: 'lemmy_alpha', password: 'lemmy', }; - let resA: Promise = fetch(`${apiUrl(alpha)}/user/login`, { + let resAlpha: Promise = fetch(`${apiUrl(alpha)}/user/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: wrapper(form), + body: wrapper(formAlpha), }).then(d => d.json()); - let formB = { + let formBeta = { username_or_email: 'lemmy_beta', password: 'lemmy', }; - let resB: Promise = fetch(`${apiUrl(beta)}/user/login`, { + let resBeta: Promise = fetch(`${apiUrl(beta)}/user/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: wrapper(formB), + body: wrapper(formBeta), }).then(d => d.json()); - let formC = { + let formGamma = { username_or_email: 'lemmy_gamma', password: 'lemmy', }; - let resG: Promise = fetch(`${apiUrl(gamma)}/user/login`, { + let resGamma: Promise = fetch(`${apiUrl(gamma)}/user/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: wrapper(formC), + body: wrapper(formGamma), }).then(d => d.json()); - let res = await Promise.all([resA, resB, resG]); + let formDelta = { + username_or_email: 'lemmy_delta', + password: 'lemmy', + }; + + let resDelta: Promise = fetch(`${apiUrl(delta)}/user/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(formDelta), + }).then(d => d.json()); + + let formEpsilon = { + username_or_email: 'lemmy_epsilon', + password: 'lemmy', + }; + + let resEpsilon: Promise = fetch( + `${apiUrl(epsilon)}/user/login`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(formEpsilon), + } + ).then(d => d.json()); + + let res = await Promise.all([ + resAlpha, + resBeta, + resGamma, + resDelta, + resEpsilon, + ]); + alpha.auth = res[0].jwt; beta.auth = res[1].jwt; gamma.auth = res[2].jwt; + delta.auth = res[3].jwt; + epsilon.auth = res[4].jwt; } export async function createPost(