From 0063bd52fb04cf129e708f8377ba343f8e06e78d Mon Sep 17 00:00:00 2001 From: Dima Krasner Date: Mon, 16 Oct 2023 18:22:14 +0300 Subject: [PATCH] display poll results and implement voting --- ap/object.go | 14 +- ap/poll_option.go | 24 ++ front/edit.go | 6 + front/graph/bars.go | 42 +++- front/post.go | 19 ++ front/print.go | 19 +- front/view.go | 28 +++ inbox/queue.go | 30 ++- test/poll_test.go | 601 ++++++++++++++++++++++++++++++++++++++++++++ test/view_test.go | 106 ++++++++ 10 files changed, 871 insertions(+), 18 deletions(-) create mode 100644 ap/poll_option.go create mode 100644 test/poll_test.go diff --git a/ap/object.go b/ap/object.go index b3e92264..03fd7539 100644 --- a/ap/object.go +++ b/ap/object.go @@ -21,9 +21,10 @@ import "time" type ObjectType string const ( - NoteObject ObjectType = "Note" - PageObject ObjectType = "Page" - ArticleObject ObjectType = "Article" + NoteObject ObjectType = "Note" + PageObject ObjectType = "Page" + ArticleObject ObjectType = "Article" + QuestionObject ObjectType = "Question" ) type Object struct { @@ -41,6 +42,13 @@ type Object struct { Tag []Mention `json:"tag,omitempty"` Attachment []Attachment `json:"attachment,omitempty"` URL string `json:"url,omitempty"` + + // polls + VotersCount int64 `json:"votersCount,omitempty"` + OneOf []PollOption `json:"oneOf,omitempty"` + AnyOf []PollOption `json:"anyOf,omitempty"` + EndTime *time.Time `json:"endTime,omitempty"` + Closed *time.Time `json:"closed,omitempty"` } func (o *Object) IsPublic() bool { diff --git a/ap/poll_option.go b/ap/poll_option.go new file mode 100644 index 00000000..ac0ec002 --- /dev/null +++ b/ap/poll_option.go @@ -0,0 +1,24 @@ +/* +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 ap + +type PollOption struct { + Name string `json:"name"` + Replies struct { + TotalItems int64 `json:"totalItems"` + } `json:"replies"` +} diff --git a/front/edit.go b/front/edit.go index c5a1e7e7..042387b6 100644 --- a/front/edit.go +++ b/front/edit.go @@ -73,6 +73,12 @@ func edit(w text.Writer, r *request) { return } + if note.Name != "" { + r.Log.Warn("Cannot edit votes", "vote", note.ID) + w.Status(40, "Cannot edit votes") + return + } + var edits int if err := r.QueryRow(`select count(*) from outbox where activity->>'object.id' = ? and (activity->>'type' = 'Update' or activity->>'type' = 'Create')`, note.ID).Scan(&edits); err != nil { r.Log.Warn("Failed to count post edits", "hash", hash, "error", err) diff --git a/front/graph/bars.go b/front/graph/bars.go index d2f3fb75..f5d93d84 100644 --- a/front/graph/bars.go +++ b/front/graph/bars.go @@ -19,22 +19,43 @@ package graph import ( "bytes" "fmt" + "unicode/utf8" ) func Bars(keys []string, values []int64) string { + flip := false var keyWidth int - for _, key := range keys { - l := len(key) - if l > keyWidth { - keyWidth = l + for i, key := range keys { + w := utf8.RuneCountInString(key) + /* + if keys have different lengths, put them on the right: the graph is + misaligned if labels contain emojis but viewed through a problematic + terminal emulator or if emoji fonts are old or missing; we must put + labels on the right to ensure that everything else is aligned + */ + if i > 0 && w > 0 && w != keyWidth { + flip = true + break + } + if w > keyWidth { + keyWidth = w } } + valueStrings := make([]string, len(values)) + + var valueWidth int var max int64 - for _, v := range values { + for i, v := range values { if v > max { max = v } + s := fmt.Sprintf("%d", v) + valueStrings[i] = s + l := len(s) + if l > valueWidth { + valueWidth = l + } } unit := float64(max) / 8 @@ -48,7 +69,9 @@ func Bars(keys []string, values []int64) string { var bar [8]rune for j, v := 0, float64(values[i]); j < 8; j, v = j+1, v-unit { - if v >= unit { + if unit == 0 { + bar[j] = ' ' + } else if v >= unit { bar[j] = '█' } else if v >= unit*7/8 { bar[j] = '▉' @@ -70,7 +93,12 @@ func Bars(keys []string, values []int64) string { bar[j] = ' ' } } - fmt.Fprintf(&w, "%-*s %8s %d\n", keyWidth, keys[i], string(bar[:]), values[i]) + + if flip { + fmt.Fprintf(&w, "%-*s %8s %s\n", valueWidth, valueStrings[i], string(bar[:]), keys[i]) + } else { + fmt.Fprintf(&w, "%-*s %8s %d\n", keyWidth, keys[i], string(bar[:]), values[i]) + } } return w.String() diff --git a/front/post.go b/front/post.go index 52294bb1..97389525 100644 --- a/front/post.go +++ b/front/post.go @@ -134,6 +134,25 @@ func post(w text.Writer, r *request, inReplyTo *ap.Object, to ap.Audience, cc ap if inReplyTo != nil { note.InReplyTo = inReplyTo.ID + + if inReplyTo.Type == ap.QuestionObject { + options := inReplyTo.OneOf + if len(options) == 0 { + options = inReplyTo.AnyOf + } + + for _, option := range options { + if option.Name == note.Content { + if inReplyTo.Closed != nil || inReplyTo.EndTime != nil && time.Now().After(*inReplyTo.EndTime) { + w.Status(40, "Cannot vote in a closed poll") + return + } + + note.Content = "" + note.Name = option.Name + } + } + } } if err := outbox.Create(r.Context, r.Log, r.DB, ¬e, r.User); err != nil { diff --git a/front/print.go b/front/print.go index 691f2c2c..6ab761bc 100644 --- a/front/print.go +++ b/front/print.go @@ -131,8 +131,10 @@ func (r *request) PrintNote(w text.Writer, note *ap.Object, author *ap.Actor, gr } noteBody := note.Content - if note.Name != "" { // Page has a title + if note.Name != "" && note.Content != "" { // Page has a title noteBody = fmt.Sprintf("%s
%s", note.Name, note.Content) + } else if note.Name != "" && note.Content == "" { // this Note is a poll vote + noteBody = note.Name } contentLines, inlineLinks := getTextAndLinks(noteBody, maxRunes, maxLines) @@ -316,10 +318,21 @@ func (r *request) PrintNote(w text.Writer, note *ap.Object, author *ap.Actor, gr return true }) - if r.User != nil && note.AttributedTo == r.User.ID { + if r.User != nil && note.AttributedTo == r.User.ID && note.Name == "" { // poll votes cannot be edited w.Link(fmt.Sprintf("/users/edit/%x", sha256.Sum256([]byte(note.ID))), "🩹 Edit") + } + if r.User != nil && note.AttributedTo == r.User.ID { w.Link(fmt.Sprintf("/users/delete/%x", sha256.Sum256([]byte(note.ID))), "💣 Delete") } + if r.User != nil && note.Type == ap.QuestionObject && note.Closed == nil && (note.EndTime == nil || time.Now().Before(*note.EndTime)) { + options := note.OneOf + if len(options) == 0 { + options = note.AnyOf + } + for _, option := range options { + w.Linkf(fmt.Sprintf("/users/reply/%x?%s", sha256.Sum256([]byte(note.ID)), url.PathEscape(option.Name)), "📮 Vote %s", option.Name) + } + } if r.User != nil { w.Link(fmt.Sprintf("/users/reply/%x", sha256.Sum256([]byte(note.ID))), "💬 Reply") } @@ -335,7 +348,7 @@ func (r *request) PrintNotes(w text.Writer, rows data.OrderedMap[string, noteMet return true } - if note.Type != ap.NoteObject && note.Type != ap.PageObject && note.Type != ap.ArticleObject { + if note.Type != ap.NoteObject && note.Type != ap.PageObject && note.Type != ap.ArticleObject && note.Type != ap.QuestionObject { r.Log.Warn("Post type is unsupported", "type", note.Type) return true } diff --git a/front/view.go b/front/view.go index 2eb96f68..c396ee30 100644 --- a/front/view.go +++ b/front/view.go @@ -24,6 +24,7 @@ import ( "fmt" "github.com/dimkr/tootik/ap" "github.com/dimkr/tootik/data" + "github.com/dimkr/tootik/front/graph" "github.com/dimkr/tootik/front/text" "path/filepath" ) @@ -122,6 +123,33 @@ func view(w text.Writer, r *request) { r.PrintNote(w, ¬e, &author, nil, false, false, true, false) } + if note.Type == ap.QuestionObject && note.VotersCount > 0 && offset == 0 { + options := note.OneOf + if len(options) == 0 { + options = note.AnyOf + } + + if len(options) > 0 { + w.Empty() + + if note.VotersCount == 1 { + w.Subtitle("📊 Results (one voter)") + } else { + w.Subtitlef("📊 Results (%d voters)", note.VotersCount) + } + + labels := make([]string, 0, len(options)) + votes := make([]int64, 0, len(options)) + + for _, option := range options { + labels = append(labels, option.Name) + votes = append(votes, option.Replies.TotalItems) + } + + w.Raw("Results graph", graph.Bars(labels, votes)) + } + } + if count > 0 && offset >= repliesPerPage { w.Empty() w.Subtitlef("💬 Replies to %s (%d-%d)", author.PreferredUsername, offset, offset+repliesPerPage) diff --git a/inbox/queue.go b/inbox/queue.go index 9849de16..c3d7525c 100644 --- a/inbox/queue.go +++ b/inbox/queue.go @@ -303,22 +303,42 @@ func processActivity(ctx context.Context, log *slog.Logger, sender *ap.Actor, re return fmt.Errorf("%s cannot update posts by %s", sender.ID, post.AttributedTo) } - var lastUpdate sql.NullInt64 - if err := db.QueryRowContext(ctx, `select max(inserted, updated) from notes where id = ? and author = ?`, post.ID, post.AttributedTo).Scan(&lastUpdate); err != nil && errors.Is(err, sql.ErrNoRows) { + var oldPostString string + var lastUpdate int64 + if err := db.QueryRowContext(ctx, `select max(inserted, updated), object from notes where id = ? and author = ?`, post.ID, post.AttributedTo).Scan(&lastUpdate, &oldPostString); err != nil && errors.Is(err, sql.ErrNoRows) { log.Debug("Received Update for non-existing post") return processCreateActivity(ctx, log, sender, req, rawActivity, post, db, resolver, from) } else if err != nil { return fmt.Errorf("Failed to get last update time for %s: %w", post.ID, err) } - if !lastUpdate.Valid || post.Updated == nil || lastUpdate.Int64 >= post.Updated.UnixNano() { + var oldPost ap.Object + if err := json.Unmarshal([]byte(oldPostString), &oldPost); err != nil { + return fmt.Errorf("Failed to unmarshal old note: %w", err) + } + + var body []byte + var err error + if (post.Type == ap.QuestionObject && post.Updated != nil && lastUpdate >= post.Updated.Unix()) || (post.Type != ap.QuestionObject && (post.Updated == nil || lastUpdate >= post.Updated.Unix())) { log.Debug("Received old update request for new post") return nil + } else if post.Type == ap.QuestionObject && oldPost.Closed != nil { + log.Debug("Received update request for closed poll") + return nil + } else if post.Type == ap.QuestionObject && post.Updated == nil { + oldPost.VotersCount = post.VotersCount + oldPost.OneOf = post.OneOf + oldPost.AnyOf = post.AnyOf + oldPost.EndTime = post.EndTime + oldPost.Closed = post.Closed + + body, err = json.Marshal(oldPost) + } else { + body, err = json.Marshal(post) } - body, err := json.Marshal(post) if err != nil { - return fmt.Errorf("Failed to update post %s: %w", post.ID, err) + return fmt.Errorf("Failed to marshal updated post %s: %w", post.ID, err) } tx, err := db.BeginTx(ctx, nil) diff --git a/test/poll_test.go b/test/poll_test.go new file mode 100644 index 00000000..81cc816b --- /dev/null +++ b/test/poll_test.go @@ -0,0 +1,601 @@ +/* +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 ( + "context" + "fmt" + "github.com/dimkr/tootik/fed" + "github.com/dimkr/tootik/inbox" + "github.com/stretchr/testify/assert" + "log/slog" + "strings" + "testing" +) + +func TestPoll_TwoOptions(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") +} + +func TestPoll_TwoOptionsZeroVotes(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":0}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":6,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (6 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "0 vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") +} + +func TestPoll_TwoOptionsOnlyZeroVotes(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":0}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":0}}],"votersCount":0,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.NotContains(view, "## 📊 Results") + assert.NotContains(strings.Split(view, "\n"), "```Results graph") +} + +func TestPoll_OneOption(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}}],"votersCount":4,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (4 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "vanilla ████████ 4") +} + +func TestPoll_Vote(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + reply := server.Handle("/users/reply/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7?vanilla", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", reply) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") + + var valid int + assert.NoError(server.db.QueryRow(`select exists (select 1 from outbox where sender = $1 and activity->>'actor' = $1 and activity->>'object.attributedTo' = $1 and activity->>'object.type' = 'Note' and activity->>'object.inReplyTo' = 'https://127.0.0.1/poll/1' and activity->>'object.name' = 'vanilla' and activity->>'object.content' is null)`, server.Alice.ID).Scan(&valid)) + assert.Equal(1, valid) +} + +func TestPoll_VoteClosedPoll(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"closed":"2020-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + reply := server.Handle("/users/reply/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7?vanilla", server.Alice) + assert.Equal("40 Cannot vote in a closed poll\r\n", reply) + + var valid int + assert.NoError(server.db.QueryRow(`select exists (select 1 from outbox where sender = $1 and activity->>'actor' = $1 and activity->>'object.attributedTo' = $1 and activity->>'object.type' = 'Note' and activity->>'object.inReplyTo' = 'https://127.0.0.1/poll/1' and activity->>'object.name' = 'vanilla' and activity->>'object.content' is null)`, server.Alice.ID).Scan(&valid)) + assert.Equal(0, valid) +} + +func TestPoll_VoteEndedPoll(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2020-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + reply := server.Handle("/users/reply/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7?vanilla", server.Alice) + assert.Equal("40 Cannot vote in a closed poll\r\n", reply) + + var valid int + assert.NoError(server.db.QueryRow(`select exists (select 1 from outbox where sender = $1 and activity->>'actor' = $1 and activity->>'object.attributedTo' = $1 and activity->>'object.type' = 'Note' and activity->>'object.inReplyTo' = 'https://127.0.0.1/poll/1' and activity->>'object.name' = 'vanilla' and activity->>'object.content' is null)`, server.Alice.ID).Scan(&valid)) + assert.Equal(0, valid) +} + +func TestPoll_Reply(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + reply := server.Handle("/users/reply/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7?strawberry", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", reply) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") + + var valid int + assert.NoError(server.db.QueryRow(`select exists (select 1 from outbox where sender = $1 and activity->>'actor' = $1 and activity->>'object.attributedTo' = $1 and activity->>'object.type' = 'Note' and activity->>'object.inReplyTo' = 'https://127.0.0.1/poll/1' and activity->>'object.name' is null and activity->>'object.content' = 'strawberry')`, server.Alice.ID).Scan(&valid)) + assert.Equal(1, valid) +} + +func TestPoll_ReplyClosedPoll(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","closed":"2020-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + reply := server.Handle("/users/reply/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7?strawberry", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", reply) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") + + var valid int + assert.NoError(server.db.QueryRow(`select exists (select 1 from outbox where sender = $1 and activity->>'actor' = $1 and activity->>'object.attributedTo' = $1 and activity->>'object.type' = 'Note' and activity->>'object.inReplyTo' = 'https://127.0.0.1/poll/1' and activity->>'object.name' is null and activity->>'object.content' = 'strawberry')`, server.Alice.ID).Scan(&valid)) + assert.Equal(1, valid) +} + +func TestPoll_EditVote(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + reply := server.Handle("/users/reply/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7?vanilla", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", reply) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") + + var valid int + assert.NoError(server.db.QueryRow(`select exists (select 1 from outbox where sender = $1 and activity->>'actor' = $1 and activity->>'object.attributedTo' = $1 and activity->>'object.type' = 'Note' and activity->>'object.inReplyTo' = 'https://127.0.0.1/poll/1' and activity->>'object.name' = 'vanilla' and activity->>'object.content' is null)`, server.Alice.ID).Scan(&valid)) + assert.Equal(1, valid) + + edit := server.Handle(fmt.Sprintf("/users/edit/%s?chocolate", reply[15:len(reply)-2]), server.Alice) + assert.Equal("40 Cannot edit votes\r\n", edit) +} + +func TestPoll_DeleteReply(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + reply := server.Handle("/users/reply/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7?strawberry", server.Alice) + assert.Regexp("^30 /users/view/[0-9a-f]{64}\r\n$", reply) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") + + var valid int + assert.NoError(server.db.QueryRow(`select exists (select 1 from outbox where sender = $1 and activity->>'actor' = $1 and activity->>'object.attributedTo' = $1 and activity->>'object.type' = 'Note' and activity->>'object.inReplyTo' = 'https://127.0.0.1/poll/1' and activity->>'object.name' is null and activity->>'object.content' = 'strawberry')`, server.Alice.ID).Scan(&valid)) + assert.Equal(1, valid) + + edit := server.Handle(fmt.Sprintf("/users/edit/%s?chocolate", reply[15:len(reply)-2]), server.Alice) + assert.Equal("40 Please try again later\r\n", edit) +} + +func TestPoll_Update(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") + + update := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/update/1","type":"Update","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":8}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":10}}],"votersCount":18,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + update, + ) + assert.NoError(err) + + n, err = inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view = server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (18 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "8 ██████▍ vanilla") + assert.Contains(strings.Split(view, "\n"), "10 ████████ chocolate") +} + +func TestPoll_OldUpdate(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") + + update := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/update/1","type":"Update","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":8}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":10}}],"votersCount":18,"endTime":"2099-10-01T05:35:36Z","updated":"2020-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + update, + ) + assert.NoError(err) + + n, err = inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view = server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") +} + +func TestPoll_UpdateClosed(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + poll := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":4}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":6}}],"votersCount":10,"endTime":"2099-10-01T05:35:36Z","closed":"2020-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + poll, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") + + update := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/update/1","type":"Update","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/poll/1","type":"Question","attributedTo":"https://127.0.0.1/user/dan","content":"vanilla or chocolate?","oneOf":[{"type":"Note","name":"vanilla","replies":{"type":"Collection","totalItems":8}},{"type":"Note","name":"chocolate","replies":{"type":"Collection","totalItems":10}}],"votersCount":18,"endTime":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + update, + ) + assert.NoError(err) + + n, err = inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view = server.Handle("/view/bc50ef0ae381c0bd8fddd856ae156bc45d83c5212669af126ea6372800f8c9d7", server.Alice) + assert.Contains(strings.Split(view, "\n"), "## 📊 Results (10 voters)") + assert.Contains(strings.Split(view, "\n"), "```Results graph") + assert.Contains(strings.Split(view, "\n"), "4 █████▎ vanilla") + assert.Contains(strings.Split(view, "\n"), "6 ████████ chocolate") +} diff --git a/test/view_test.go b/test/view_test.go index 12ddc4ee..ea57f3b4 100644 --- a/test/view_test.go +++ b/test/view_test.go @@ -17,9 +17,13 @@ limitations under the License. package test import ( + "context" "crypto/sha256" "fmt" + "github.com/dimkr/tootik/fed" + "github.com/dimkr/tootik/inbox" "github.com/stretchr/testify/assert" + "log/slog" "testing" ) @@ -297,3 +301,105 @@ func TestView_InvalidOffset(t *testing.T) { view := server.Handle("/users/view/87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7?z", server.Bob) assert.Equal("40 Invalid query\r\n", view) } + +func TestView_Update(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + create := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/note/1","type":"Note","attributedTo":"https://127.0.0.1/user/dan","content":"hello","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + create, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/users/view/ff2b86e2dbb0cc086c97f1cf9b4398c26959821cddafdcd387c4471e6ec8cd65", server.Alice) + assert.Contains(view, "hello") + assert.NotContains(view, "bye") + assert.NotContains(view, "edited") + + update := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/update/1","type":"Update","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/note/1","type":"Note","attributedTo":"https://127.0.0.1/user/dan","content":"bye","updated":"2099-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + update, + ) + assert.NoError(err) + + n, err = inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view = server.Handle("/users/view/ff2b86e2dbb0cc086c97f1cf9b4398c26959821cddafdcd387c4471e6ec8cd65", server.Alice) + assert.NotContains(view, "hello") + assert.Contains(view, "bye") + assert.Contains(view, "edited") +} + +func TestView_OldUpdate(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + _, err := server.db.Exec( + `insert into persons (id, hash, actor) values(?,?,?)`, + "https://127.0.0.1/user/dan", + "eab50d465047c1ccfc581759f33612c583486044f5de62b2a5e77e220c2f1ae3", + `{"type":"Person","preferredUsername":"dan"}`, + ) + assert.NoError(err) + + create := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/create/1","type":"Create","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/note/1","type":"Note","attributedTo":"https://127.0.0.1/user/dan","content":"hello","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + create, + ) + assert.NoError(err) + + n, err := inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view := server.Handle("/users/view/ff2b86e2dbb0cc086c97f1cf9b4398c26959821cddafdcd387c4471e6ec8cd65", server.Alice) + assert.Contains(view, "hello") + assert.NotContains(view, "bye") + assert.NotContains(view, "edited") + + update := `{"@context":["https://www.w3.org/ns/activitystreams"],"id":"https://127.0.0.1/update/1","type":"Update","actor":"https://127.0.0.1/user/dan","object":{"id":"https://127.0.0.1/note/1","type":"Note","attributedTo":"https://127.0.0.1/user/dan","content":"bye","updated":"2020-10-01T05:35:36Z","to":["https://www.w3.org/ns/activitystreams#Public"]},"to":["https://www.w3.org/ns/activitystreams#Public"]}` + + _, err = server.db.Exec( + `insert into inbox (sender, activity) values(?,?)`, + "https://127.0.0.1/user/dan", + update, + ) + assert.NoError(err) + + n, err = inbox.ProcessBatch(context.Background(), slog.Default(), server.db, fed.NewResolver(nil), server.Nobody) + assert.NoError(err) + assert.Equal(1, n) + + view = server.Handle("/users/view/ff2b86e2dbb0cc086c97f1cf9b4398c26959821cddafdcd387c4471e6ec8cd65", server.Alice) + assert.Contains(view, "hello") + assert.NotContains(view, "bye") + assert.NotContains(view, "edited") +}