mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
525 lines
19 KiB
Go
525 lines
19 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/goccy/go-yaml"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/shared/mlog"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
)
|
|
|
|
// readReportZip opens the ZIP archive at path and returns a map of entry name
|
|
// to contents. The temp file produced by GenerateFlaggedPostReport is also
|
|
// removed from disk after reading so individual tests don't have to clean up.
|
|
func readReportZip(t *testing.T, path string) map[string][]byte {
|
|
t.Helper()
|
|
defer func() {
|
|
_ = os.Remove(path)
|
|
}()
|
|
|
|
zr, err := zip.OpenReader(path)
|
|
require.NoError(t, err)
|
|
defer zr.Close()
|
|
|
|
out := map[string][]byte{}
|
|
for _, f := range zr.File {
|
|
rc, err := f.Open()
|
|
require.NoError(t, err)
|
|
b, err := io.ReadAll(rc)
|
|
require.NoError(t, err)
|
|
_ = rc.Close()
|
|
out[f.Name] = b
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestGenerateFlaggedPostReport(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("returns error when post does not exist", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, model.NewId(), th.BasicUser.Id, "", "")
|
|
require.NotNil(t, appErr)
|
|
require.Empty(t, path)
|
|
})
|
|
|
|
t.Run("returns error when generator user does not exist", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, model.NewId(), "", "")
|
|
require.NotNil(t, appErr)
|
|
require.Empty(t, path)
|
|
})
|
|
|
|
t.Run("produces a zip with the expected entries for a basic flagged post", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
require.NotEmpty(t, path)
|
|
|
|
_, statErr := os.Stat(path)
|
|
require.NoError(t, statErr, "report temp file should exist on disk until caller removes it")
|
|
|
|
entries := readReportZip(t, path)
|
|
require.Contains(t, entries, "post/post.yaml")
|
|
require.Contains(t, entries, "content_review.yaml")
|
|
require.Contains(t, entries, "report_metadata.yaml")
|
|
})
|
|
|
|
t.Run("post.yaml contains channel, team, and author details", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
var payload map[string]any
|
|
require.NoError(t, yaml.Unmarshal(entries["post/post.yaml"], &payload))
|
|
require.Equal(t, post.Id, payload["id"])
|
|
require.Equal(t, post.UserId, payload["author_id"])
|
|
require.Equal(t, th.BasicUser.Username, payload["author_name"])
|
|
require.Equal(t, th.BasicChannel.DisplayName, payload["channel_display_name"])
|
|
require.Equal(t, th.BasicTeam.Id, payload["team_id"])
|
|
require.Equal(t, th.BasicTeam.DisplayName, payload["team_display_name"])
|
|
})
|
|
|
|
t.Run("report_metadata.yaml records the generating user and a non-zero timestamp", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
var meta model.FlaggedPostReportMetadata
|
|
require.NoError(t, yaml.Unmarshal(entries["report_metadata.yaml"], &meta))
|
|
require.Equal(t, th.BasicUser.Id, meta.GeneratedByUserID)
|
|
require.Equal(t, th.BasicUser.Username, meta.GeneratedByUsername)
|
|
require.Equal(t, model.FlaggedPostReportVersion, meta.ReportVersion)
|
|
require.Greater(t, meta.Timestamp, int64(0))
|
|
})
|
|
|
|
t.Run("content_review.yaml records reporter details", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
var review model.FlaggedPostReportContentReview
|
|
require.NoError(t, yaml.Unmarshal(entries["content_review.yaml"], &review))
|
|
// setupFlaggedPost flags as BasicUser2 with reason "spam".
|
|
require.Equal(t, th.BasicUser2.Id, review.ReporterUserID)
|
|
require.Equal(t, th.BasicUser2.Username, review.ReporterUsername)
|
|
require.Equal(t, "spam", review.ReporterReason)
|
|
require.Equal(t, "This is spam content", review.ReporterComment)
|
|
require.Greater(t, review.ReportTimestamp, int64(0))
|
|
require.Empty(t, review.ActorDecision)
|
|
require.Empty(t, review.ActorUserId)
|
|
require.Empty(t, review.ActorUsername)
|
|
})
|
|
|
|
t.Run("content_review.yaml records remove decision and committed actor after permanent delete", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, &model.FlagContentActionRequest{Comment: "violates policy"}, th.SystemAdminUser.Id, post)
|
|
require.Nil(t, appErr)
|
|
|
|
// Report is generated by BasicUser, but the committed actor on the post is SystemAdminUser.
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
var review model.FlaggedPostReportContentReview
|
|
require.NoError(t, yaml.Unmarshal(entries["content_review.yaml"], &review))
|
|
require.Equal(t, "remove", review.ActorDecision)
|
|
require.Equal(t, th.SystemAdminUser.Id, review.ActorUserId)
|
|
require.Equal(t, th.SystemAdminUser.Username, review.ActorUsername)
|
|
})
|
|
|
|
t.Run("content_review.yaml records keep decision and committed actor after keep action", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
appErr = th.App.KeepFlaggedPost(th.Context, &model.FlagContentActionRequest{Comment: "looks fine"}, th.SystemAdminUser.Id, post)
|
|
require.Nil(t, appErr)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
var review model.FlaggedPostReportContentReview
|
|
require.NoError(t, yaml.Unmarshal(entries["content_review.yaml"], &review))
|
|
require.Equal(t, "keep", review.ActorDecision)
|
|
require.Equal(t, th.SystemAdminUser.Id, review.ActorUserId)
|
|
require.Equal(t, th.SystemAdminUser.Username, review.ActorUsername)
|
|
})
|
|
|
|
t.Run("content_review.yaml prefers committed actor over pending action's generator", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
// Commit a keep action so the actor user id property is set to SystemAdminUser.
|
|
appErr = th.App.KeepFlaggedPost(th.Context, &model.FlagContentActionRequest{Comment: "looks fine"}, th.SystemAdminUser.Id, post)
|
|
require.Nil(t, appErr)
|
|
|
|
// Even with a pending action supplied (and a different generator), the
|
|
// committed actor on the post should win over the pending-action fallback.
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", model.ContentFlaggingActionRemove)
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
var review model.FlaggedPostReportContentReview
|
|
require.NoError(t, yaml.Unmarshal(entries["content_review.yaml"], &review))
|
|
require.Equal(t, th.SystemAdminUser.Id, review.ActorUserId)
|
|
require.Equal(t, th.SystemAdminUser.Username, review.ActorUsername)
|
|
})
|
|
|
|
t.Run("content_review.yaml uses pending action when status is not yet committed", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", model.ContentFlaggingActionRemove)
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
var review model.FlaggedPostReportContentReview
|
|
require.NoError(t, yaml.Unmarshal(entries["content_review.yaml"], &review))
|
|
require.Equal(t, "remove", review.ActorDecision)
|
|
require.Equal(t, th.BasicUser.Id, review.ActorUserId)
|
|
require.Equal(t, th.BasicUser.Username, review.ActorUsername)
|
|
})
|
|
|
|
t.Run("content_review.yaml ignores invalid pending action", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "bogus")
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
var review model.FlaggedPostReportContentReview
|
|
require.NoError(t, yaml.Unmarshal(entries["content_review.yaml"], &review))
|
|
require.Empty(t, review.ActorDecision)
|
|
// Actor details are still populated whenever a pending action is supplied,
|
|
// even when the value isn't a recognised decision.
|
|
require.Equal(t, th.BasicUser.Id, review.ActorUserId)
|
|
require.Equal(t, th.BasicUser.Username, review.ActorUsername)
|
|
})
|
|
|
|
t.Run("includes file attachments for the base post", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := th.CreatePost(t, th.BasicChannel)
|
|
|
|
attachmentBody := []byte("hello attachment body")
|
|
fileInfo := &model.FileInfo{
|
|
Id: model.NewId(),
|
|
PostId: post.Id,
|
|
CreatorId: post.UserId,
|
|
Path: "test/" + model.NewId() + "/attachment.txt",
|
|
Name: "attachment.txt",
|
|
Size: int64(len(attachmentBody)),
|
|
}
|
|
_, appErr = th.App.WriteFile(bytes.NewReader(attachmentBody), fileInfo.Path)
|
|
require.Nil(t, appErr)
|
|
t.Cleanup(func() { _ = th.App.RemoveFile(fileInfo.Path) })
|
|
|
|
_, err := th.App.Srv().Store().FileInfo().Save(th.Context, fileInfo)
|
|
require.NoError(t, err)
|
|
|
|
// Persist FileIds directly via the store. UpdatePost would route through
|
|
// AttachToPost, which is a no-op here because the FileInfo row already has
|
|
// PostId set, leaving post.FileIds empty in the DB.
|
|
post.FileIds = []string{fileInfo.Id}
|
|
_, err = th.App.Srv().Store().Post().Overwrite(th.Context, post)
|
|
require.NoError(t, err)
|
|
|
|
flagData := model.FlagContentRequest{
|
|
Reason: "spam",
|
|
Comment: "This is spam content",
|
|
}
|
|
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
|
|
require.Nil(t, appErr)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
entryName := "post/attachments/" + fileInfo.Id + "_" + fileInfo.Name
|
|
require.Contains(t, entries, entryName)
|
|
require.Equal(t, attachmentBody, entries[entryName])
|
|
require.Contains(t, entries, "post/post.yaml")
|
|
require.Contains(t, entries, "content_review.yaml")
|
|
require.Contains(t, entries, "report_metadata.yaml")
|
|
})
|
|
|
|
t.Run("includes edit history entries when the post has been edited", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := th.CreatePost(t, th.BasicChannel)
|
|
|
|
edited := post.Clone()
|
|
edited.Message = "Edited message"
|
|
edited.EditAt = model.GetMillis()
|
|
_, _, appErr = th.App.UpdatePost(th.Context, edited, &model.UpdatePostOptions{})
|
|
require.Nil(t, appErr)
|
|
|
|
editHistory, appErr := th.App.GetEditHistoryForPost(post.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotEmpty(t, editHistory)
|
|
editID := editHistory[0].Id
|
|
|
|
flagData := model.FlagContentRequest{
|
|
Reason: "spam",
|
|
Comment: "This is spam content",
|
|
}
|
|
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
|
|
require.Nil(t, appErr)
|
|
|
|
path, appErr := th.App.GenerateFlaggedPostReport(th.Context, post.Id, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
|
|
entries := readReportZip(t, path)
|
|
require.Contains(t, entries, "edit_history/"+editID+"/post.yaml")
|
|
|
|
// The base post.yaml should also list the edit history order.
|
|
var basePayload map[string]any
|
|
require.NoError(t, yaml.Unmarshal(entries["post/post.yaml"], &basePayload))
|
|
order, ok := basePayload["edit_history_order"].([]any)
|
|
require.True(t, ok, "edit_history_order should be present on the base post payload")
|
|
require.Contains(t, order, editID)
|
|
})
|
|
}
|
|
|
|
// TestBuildContentReviewYAMLActorCommentFallback exercises the replica-lag
|
|
// fallback in buildContentReviewYAML: when the property-value read (which is
|
|
// served from a read replica) returns no actor_comment value, the function
|
|
// must fall back to the in-hand actorComment supplied by the caller so the
|
|
// generated report still reflects the comment the user just submitted.
|
|
func TestBuildContentReviewYAMLActorCommentFallback(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("falls back to in-hand comment when no actor_comment is persisted", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
// setupFlaggedPost only writes the reporting_* properties — no
|
|
// actor_comment row exists yet, so the replica read returns "" for
|
|
// that field and the fallback branch must take effect.
|
|
review, appErr := th.App.buildContentReviewYAML(th.Context, post, th.BasicUser.Id, "fallback note", "")
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, "fallback note", review.ReviewerComment)
|
|
})
|
|
|
|
t.Run("reviewer comment stays empty when both sources are empty", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
review, appErr := th.App.buildContentReviewYAML(th.Context, post, th.BasicUser.Id, "", "")
|
|
require.Nil(t, appErr)
|
|
require.Empty(t, review.ReviewerComment)
|
|
})
|
|
}
|
|
|
|
func TestBuildPostYAML(t *testing.T) {
|
|
channel := &model.Channel{Id: "channel-id", DisplayName: "Channel Name"}
|
|
team := &model.Team{Id: "team-id", DisplayName: "Team Name"}
|
|
author := &model.User{Id: "author-id", Username: "alice", Email: "[email protected]"}
|
|
|
|
t.Run("populates author, channel, and team fields", func(t *testing.T) {
|
|
post := &model.Post{Id: "post-id", UserId: author.Id, ChannelId: channel.Id, Message: "hi"}
|
|
|
|
got := buildPostYAML(post, channel, team, author, nil)
|
|
|
|
require.Equal(t, "alice", got.AuthorName)
|
|
require.Equal(t, "[email protected]", got.AuthorEmail)
|
|
require.Equal(t, "Channel Name", got.ChannelDisplayName)
|
|
require.Equal(t, "team-id", got.TeamID)
|
|
require.Equal(t, "Team Name", got.TeamDisplayName)
|
|
})
|
|
|
|
t.Run("omits team fields for DM/GM posts", func(t *testing.T) {
|
|
post := &model.Post{Id: "post-id", UserId: author.Id, ChannelId: channel.Id}
|
|
|
|
got := buildPostYAML(post, channel, nil, author, nil)
|
|
|
|
require.Empty(t, got.TeamID)
|
|
require.Empty(t, got.TeamDisplayName)
|
|
})
|
|
|
|
t.Run("omits author fields when author is nil", func(t *testing.T) {
|
|
post := &model.Post{Id: "post-id", ChannelId: channel.Id}
|
|
|
|
got := buildPostYAML(post, channel, team, nil, nil)
|
|
|
|
require.Empty(t, got.AuthorName)
|
|
require.Empty(t, got.AuthorEmail)
|
|
})
|
|
|
|
t.Run("populates reply count only for root posts", func(t *testing.T) {
|
|
root := &model.Post{Id: "root", ChannelId: channel.Id, ReplyCount: 3}
|
|
got := buildPostYAML(root, channel, team, author, nil)
|
|
require.NotNil(t, got.ReplyCountPtr)
|
|
require.Equal(t, int64(3), *got.ReplyCountPtr)
|
|
|
|
reply := &model.Post{Id: "reply", ChannelId: channel.Id, RootId: "root", ReplyCount: 0}
|
|
got = buildPostYAML(reply, channel, team, author, nil)
|
|
require.Nil(t, got.ReplyCountPtr)
|
|
})
|
|
|
|
t.Run("preserves edit history order verbatim", func(t *testing.T) {
|
|
post := &model.Post{Id: "post-id", ChannelId: channel.Id}
|
|
order := []string{"edit-3", "edit-2", "edit-1"}
|
|
|
|
got := buildPostYAML(post, channel, team, author, order)
|
|
|
|
require.Equal(t, order, got.EditHistoryOrder)
|
|
})
|
|
}
|
|
|
|
func TestAttachmentEntryName(t *testing.T) {
|
|
t.Run("prefixes the file id and keeps the original name", func(t *testing.T) {
|
|
fi := &model.FileInfo{Id: "abc123", Name: "report.pdf"}
|
|
require.Equal(t, "abc123_report.pdf", attachmentEntryName(fi))
|
|
})
|
|
|
|
t.Run("strips forward and back slashes to prevent path traversal", func(t *testing.T) {
|
|
fi := &model.FileInfo{Id: "abc123", Name: "../../etc/passwd"}
|
|
require.Equal(t, "abc123_.._.._etc_passwd", attachmentEntryName(fi))
|
|
|
|
fi = &model.FileInfo{Id: "abc123", Name: `..\..\windows\system32`}
|
|
require.Equal(t, "abc123_.._.._windows_system32", attachmentEntryName(fi))
|
|
})
|
|
|
|
t.Run("falls back to a synthesised name when the original is empty", func(t *testing.T) {
|
|
fi := &model.FileInfo{Id: "abc123", Name: "", Extension: "pdf"}
|
|
require.Equal(t, "abc123_attachment.pdf", attachmentEntryName(fi))
|
|
|
|
fi = &model.FileInfo{Id: "abc123", Name: " ", Extension: ""}
|
|
require.Equal(t, "abc123_attachment", attachmentEntryName(fi))
|
|
})
|
|
}
|
|
|
|
func TestDecodePropertyString(t *testing.T) {
|
|
rctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t))
|
|
|
|
t.Run("returns empty string when key is missing", func(t *testing.T) {
|
|
got := decodePropertyString(rctx, map[string]json.RawMessage{}, "missing")
|
|
require.Empty(t, got)
|
|
})
|
|
|
|
t.Run("returns empty string for empty raw value", func(t *testing.T) {
|
|
got := decodePropertyString(rctx, map[string]json.RawMessage{"k": []byte{}}, "k")
|
|
require.Empty(t, got)
|
|
})
|
|
|
|
t.Run("decodes a JSON-encoded string", func(t *testing.T) {
|
|
got := decodePropertyString(rctx, map[string]json.RawMessage{"k": json.RawMessage(`"hello \"world\""`)}, "k")
|
|
require.Equal(t, `hello "world"`, got)
|
|
})
|
|
|
|
t.Run("returns empty string when the raw value is not valid JSON", func(t *testing.T) {
|
|
got := decodePropertyString(rctx, map[string]json.RawMessage{"k": json.RawMessage("not-json")}, "k")
|
|
require.Empty(t, got)
|
|
})
|
|
}
|
|
|
|
func TestDecodePropertyInt64(t *testing.T) {
|
|
rctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t))
|
|
|
|
t.Run("returns zero when key is missing", func(t *testing.T) {
|
|
got := decodePropertyInt64(rctx, map[string]json.RawMessage{}, "missing")
|
|
require.Equal(t, int64(0), got)
|
|
})
|
|
|
|
t.Run("returns zero for empty raw value", func(t *testing.T) {
|
|
got := decodePropertyInt64(rctx, map[string]json.RawMessage{"k": []byte{}}, "k")
|
|
require.Equal(t, int64(0), got)
|
|
})
|
|
|
|
t.Run("decodes a JSON-encoded number", func(t *testing.T) {
|
|
got := decodePropertyInt64(rctx, map[string]json.RawMessage{"k": json.RawMessage("12345")}, "k")
|
|
require.Equal(t, int64(12345), got)
|
|
})
|
|
|
|
t.Run("returns zero when the raw value is not a JSON number", func(t *testing.T) {
|
|
got := decodePropertyInt64(rctx, map[string]json.RawMessage{"k": json.RawMessage(`"12345"`)}, "k")
|
|
require.Equal(t, int64(0), got)
|
|
})
|
|
}
|
|
|
|
func TestNotifyReviewersOfFlaggedPostReportGeneration(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("does not panic when called for a non-flagged post", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
// Use a random post id to exercise the best-effort error path. The function
|
|
// should swallow any errors and return without raising.
|
|
require.NotPanics(t, func() {
|
|
th.App.NotifyReviewersOfFlaggedPostReportGeneration(th.Context, model.NewId(), th.BasicUser.Id)
|
|
})
|
|
})
|
|
|
|
t.Run("does not panic when generator user does not exist", func(t *testing.T) {
|
|
appErr := setBaseConfig(th)
|
|
require.Nil(t, appErr)
|
|
|
|
post := setupFlaggedPost(t, th)
|
|
|
|
require.NotPanics(t, func() {
|
|
th.App.NotifyReviewersOfFlaggedPostReportGeneration(th.Context, post.Id, model.NewId())
|
|
})
|
|
})
|
|
}
|