diff --git a/front/outbox.go b/front/outbox.go index 15e0f42a..52c6ccc4 100644 --- a/front/outbox.go +++ b/front/outbox.go @@ -61,11 +61,105 @@ func userOutbox(w text.Writer, r *request) { r.Log.Info("Viewing outbox", "offset", offset) var rows *sql.Rows - if actor.Type == ap.Group { - // if this is a group, show posts sent to this group instead of showing posts by the group - rows, err = r.Query(`select object, actor, null from (select notes.object, persons.actor from (select object, author, inserted from notes where public = 1 and $1 in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) and object->'inReplyTo' is null) notes join persons on persons.id = notes.author order by notes.inserted desc limit $2 offset $3)`, actorID, postsPerPage, offset) + if actor.Type == ap.Group && r.User == nil { + // unauthenticated users can only see public posts in a group + rows, err = r.Query( + `select notes.object, persons.actor, null from ( + select object, author from notes where groupid = $1 and public = 1 and object->'inReplyTo' is null + order by notes.inserted desc limit $2 offset $3 + ) notes + join persons on persons.id = notes.author`, + actorID, + postsPerPage, + offset, + ) + } else if actor.Type == ap.Group && r.User != nil { + // users can see public posts in a group and non-public posts if they follow the group + rows, err = r.Query(` + select notes.object, persons.actor, null from ( + select notes.object, notes.author from notes + where + groupid = $1 and + ( + public = 1 or + exists (select 1 from follows where follower = $2 and followed = $1 and accepted = 1) + ) and + object->'inReplyTo' is null + order by inserted desc limit $3 offset $4 + ) notes + join persons on persons.id = notes.author`, + actorID, + r.User.ID, + postsPerPage, + offset, + ) + } else if r.User == nil { + // unauthenticated users can only see public posts + rows, err = r.Query( + `select notes.object, $1, groups.actor from ( + select object, inserted, groupid from notes + where author = $2 and public = 1 + order by notes.inserted desc limit $3 offset $4 + ) notes + left join ( + select id, actor from persons where actor->>'type' = 'Group' + ) groups on groups.id = notes.groupid`, + actorString, + actorID, + postsPerPage, offset, + ) + } else if r.User.ID == actorID { + // users can see all their posts + rows, err = r.Query( + `select notes.object, $1, groups.actor from ( + select object, inserted, groupid from notes + where author = $2 + order by notes.inserted desc limit $3 offset $4 + ) notes + left join ( + select id, actor from persons where actor->>'type' = 'Group' + ) groups on groups.id = notes.groupid`, + actorString, + actorID, + postsPerPage, + offset, + ) } else { - rows, err = r.Query(`select object, $1, g from (select notes.object, notes.inserted, groups.actor as g from (select id, object, inserted, groupid from notes where public = 1 and author = $2 union select notes.id, notes.object, notes.inserted, notes.groupid from notes join persons on persons.actor->>'followers' in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) or (notes.to2 is not null and exists (select 1 from json_each(notes.object->'to') where value = persons.actor->>'followers')) or (notes.cc2 is not null and exists (select 1 from json_each(notes.object->'cc') where value = persons.actor->>'followers')) where notes.public = 0 and notes.author = $2 and persons.id = $2) notes left join (select id, actor from persons where actor->>'type' = 'Group') groups on groups.id = notes.groupid group by notes.id) order by inserted desc limit $3 offset $4`, actorString, actorID, postsPerPage, offset) + // users can see only public posts by others, posts to followers if following, and DMs + rows, err = r.Query( + `select u.object, $1, groups.actor from ( + select object, inserted, groupid from notes + where public = 1 and author = $2 + union + select object, inserted, groupid from notes + where ( + author = $2 and ( + $3 in (cc0, to0, cc1, to1, cc2, to2) or + (to2 is not null and exists (select 1 from json_each(object->'to') where value = $3)) or + (cc2 is not null and exists (select 1 from json_each(object->'cc') where value = $3)) + ) + ) + union + select object, notes.inserted, groupid from notes + join persons on + persons.actor->>'followers' in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) or + (notes.to2 is not null and exists (select 1 from json_each(notes.object->'to') where value = persons.actor->>'followers')) or + (notes.cc2 is not null and exists (select 1 from json_each(notes.object->'cc') where value = persons.actor->>'followers')) + where notes.public = 0 and + notes.author = $2 and + persons.id = $2 and + exists (select 1 from follows where follower = $3 and followed = $2 and accepted = 1) + order by inserted desc limit $4 offset $5 + ) u + left join ( + select id, actor from persons where actor->>'type' = 'Group' + ) groups on groups.id = u.groupid`, + actorString, + actorID, + r.User.ID, + postsPerPage, + offset, + ) } if err != nil { r.Log.Warn("Failed to fetch posts", "error", err) diff --git a/test/outbox_test.go b/test/outbox_test.go new file mode 100644 index 00000000..279d6ada --- /dev/null +++ b/test/outbox_test.go @@ -0,0 +1,634 @@ +/* +Copyright 2023 Dima Krasner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "crypto/sha256" + "fmt" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func TestOutbox_NonExistingUser(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + outbox := server.Handle("/users/outbox/1393ac075483a094823f4b88bc18accca757c2f9e68ca6bc6aa14fc841a292e4", server.Bob) + assert.Equal("40 User not found\r\n", outbox) +} + +func TestOutbox_InvalidOffset(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + say := server.Handle("/users/say?Hello%20world", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", say) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x?abc", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal("40 Invalid query\r\n", outbox) +} + +func TestOutbox_PublicPost(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + say := server.Handle("/users/say?Hello%20world", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", say) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Contains(outbox, "Hello world") +} + +func TestOutbox_PublicPostUnauthenticatedUser(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + say := server.Handle("/users/say?Hello%20world", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", say) + + outbox := server.Handle(fmt.Sprintf("/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), nil) + assert.Contains(outbox, "Hello world") +} + +func TestOutbox_PublicPostSelf(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + say := server.Handle("/users/say?Hello%20world", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", say) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Alice) + assert.Contains(outbox, "Hello world") +} + +func TestOutbox_PostToFollowers(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + follow := server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle("/users/whisper?Hello%20world", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", whisper) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Contains(outbox, "Hello world") +} + +func TestOutbox_PostToFollowersNotFollowing(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + whisper := server.Handle("/users/whisper?Hello%20world", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", whisper) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Contains(strings.Split(outbox, "\n"), "No posts.") + assert.NotContains(outbox, "Hello world") +} + +func TestOutbox_PostToFollowersUnauthentictedUser(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + whisper := server.Handle("/users/whisper?Hello%20world", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", whisper) + + outbox := server.Handle(fmt.Sprintf("/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), nil) + assert.Contains(strings.Split(outbox, "\n"), "No posts.") + assert.NotContains(outbox, "Hello world") +} + +func TestOutbox_PostToFollowersSelf(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + whisper := server.Handle("/users/whisper?Hello%20world", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", whisper) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Alice) + assert.Contains(outbox, "Hello world") +} + +func TestOutbox_DM(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + follow := server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + dm := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", dm) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Contains(outbox, "Hello bob") +} + +func TestOutbox_DMSelf(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + follow := server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + dm := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", dm) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Alice) + assert.Contains(outbox, "Hello bob") +} + +func TestOutbox_DMNotRecipient(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + follow := server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + dm := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", dm) + + outbox := server.Handle(fmt.Sprintf("/users/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Carol) + assert.NotContains(outbox, "Hello bob") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +} + +func TestOutbox_UnauthenticatedUser(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + follow := server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + dm := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", dm) + + outbox := server.Handle(fmt.Sprintf("/outbox/%x", sha256.Sum256([]byte(server.Alice.ID))), nil) + assert.NotContains(outbox, "Hello bob") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +} + +func TestOutbox_PublicPostInGroup(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + say := server.Handle("/users/say?Hello%20people%20in%20%40people%40other.localdomain", server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", say) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Contains(outbox, "Hello people in @people@other.localdomain") +} + +func TestOutbox_PublicPostInGroupUnauthenticatedUser(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + say := server.Handle("/users/say?Hello%20people%20in%20%40people%40other.localdomain", server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", say) + + outbox := server.Handle("/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", nil) + assert.Contains(outbox, "Hello people in @people@other.localdomain") +} + +func TestOutbox_PostToFollowersInGroup(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + whisper := server.Handle("/users/whisper?Hello%20people%20in%20%40people%40other.localdomain", server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Contains(outbox, "Hello people in @people@other.localdomain") +} + +func TestOutbox_PostToFollowersInGroupNotFollowingGroup(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + whisper := server.Handle("/users/whisper?Hello%20people%20in%20%40people%40other.localdomain", server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.NotContains(outbox, "Hello people in @people@other.localdomain") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +} + +func TestOutbox_PostToFollowersInGroupNotAccepted(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + whisper := server.Handle("/users/whisper?Hello%20people%20in%20%40people%40other.localdomain", server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.NotContains(outbox, "Hello people in @people@other.localdomain") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +} + +func TestOutbox_PostToFollowersInGroupFollowingAuthor(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle("/users/whisper?Hello%20people%20in%20%40people%40other.localdomain", server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.NotContains(outbox, "Hello people in @people@other.localdomain") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +} + +func TestOutbox_PostToFollowersInGroupUnauthenticatedUser(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + whisper := server.Handle("/users/whisper?Hello%20people%20in%20%40people%40other.localdomain", server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", nil) + assert.NotContains(outbox, "Hello people in @people@other.localdomain") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +} + +func TestOutbox_DMInGroup(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob%%20from%%20%%40people%%40other.localdomain", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Contains(outbox, "Hello bob from @people@other.localdomain") +} + +func TestOutbox_DMInGroupNotFollowingGroup(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob%%20from%%20%%40people%%40other.localdomain", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.NotContains(outbox, "Hello bob from @people@other.localdomain") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +} + +func TestOutbox_DMInGroupAnotherUser(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Carol) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob%%20from%%20%%40people%%40other.localdomain", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Carol) + assert.Contains(outbox, "Hello bob from @people@other.localdomain") +} + +func TestOutbox_DMInGroupSelf(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob%%20from%%20%%40people%%40other.localdomain", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Contains(outbox, "Hello bob from @people@other.localdomain") +} + +func TestOutbox_DMInGroupSelfGroupUnfollowed(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob%%20from%%20%%40people%%40other.localdomain", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + unfollow := server.Handle("/users/unfollow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", unfollow) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.NotContains(outbox, "Hello people in @people@other.localdomain") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +} + +func TestOutbox_DMInGroupSelfRecipientUnfollowed(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob%%20from%%20%%40people%%40other.localdomain", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + unfollow := server.Handle(fmt.Sprintf("/users/unfollow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), unfollow) + + outbox := server.Handle("/users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Contains(outbox, "Hello bob from @people@other.localdomain") +} + +func TestOutbox_DMInGroupAnauthenticatedUser(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://other.localdomain/group/people", + "4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", + `{"type":"Group","preferredUsername":"people"}`, + ) + assert.NoError(err) + + follow := server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Alice) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + follow = server.Handle("/users/follow/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", server.Bob) + assert.Equal("30 /users/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f\r\n", follow) + + _, err = server.db.Exec(`update follows set accepted = 1`) + assert.NoError(err) + + follow = server.Handle(fmt.Sprintf("/users/follow/%x", sha256.Sum256([]byte(server.Alice.ID))), server.Bob) + assert.Equal(fmt.Sprintf("30 /users/outbox/%x\r\n", sha256.Sum256([]byte(server.Alice.ID))), follow) + + whisper := server.Handle(fmt.Sprintf("/users/dm/%x?Hello%%20bob%%20from%%20%%40people%%40other.localdomain", sha256.Sum256([]byte(server.Bob.ID))), server.Alice) + assert.Regexp("30 /users/view/[0-9a-f]{64}", whisper) + + outbox := server.Handle("/outbox/4eeaa25305ef85dec1dc646e02f54fc1702f594d5bc0c8b9b1c41595a16ea70f", nil) + assert.NotContains(outbox, "Hello people in @people@other.localdomain") + assert.Contains(strings.Split(outbox, "\n"), "No posts.") +}