diff --git a/models/issues/moderation.go b/models/issues/moderation.go index 9afb711d65..0a66a1d24c 100644 --- a/models/issues/moderation.go +++ b/models/issues/moderation.go @@ -27,19 +27,24 @@ type IssueData struct { // Implements GetFieldsMap() from ShadowCopyData interface, returning a list of pairs // to be used when rendering the shadow copy for admins reviewing the corresponding abuse report(s). -func (cd IssueData) GetFieldsMap() []moderation.ShadowCopyField { +func (id IssueData) GetFieldsMap() []moderation.ShadowCopyField { return []moderation.ShadowCopyField{ - {Key: "RepoID", Value: strconv.FormatInt(cd.RepoID, 10)}, - {Key: "Index", Value: strconv.FormatInt(cd.Index, 10)}, - {Key: "PosterID", Value: strconv.FormatInt(cd.PosterID, 10)}, - {Key: "Title", Value: cd.Title}, - {Key: "Content", Value: cd.Content}, - {Key: "ContentVersion", Value: strconv.Itoa(cd.ContentVersion)}, - {Key: "CreatedUnix", Value: cd.CreatedUnix.AsLocalTime().String()}, - {Key: "UpdatedUnix", Value: cd.UpdatedUnix.AsLocalTime().String()}, + {Key: "RepoID", Value: strconv.FormatInt(id.RepoID, 10)}, + {Key: "Index", Value: strconv.FormatInt(id.Index, 10)}, + {Key: "Poster", Value: strconv.FormatInt(id.PosterID, 10)}, + {Key: "Title", Value: id.Title}, + {Key: "Content", Value: id.Content}, + {Key: "ContentVersion", Value: strconv.Itoa(id.ContentVersion)}, + {Key: "CreatedUnix", Value: id.CreatedUnix.AsLocalTime().String()}, + {Key: "UpdatedUnix", Value: id.UpdatedUnix.AsLocalTime().String()}, } } +// Implements GetAbuserID() from ShadowCopyData interface, returning the value of PosterID field. +func (id *IssueData) GetAbuserID() (int64, bool) { + return id.PosterID, true +} + // newIssueData creates a trimmed down issue to be used just to create a JSON structure // (keeping only the fields relevant for moderation purposes) func newIssueData(issue *Issue) IssueData { @@ -70,7 +75,7 @@ type CommentData struct { // to be used when rendering the shadow copy for admins reviewing the corresponding abuse report(s). func (cd CommentData) GetFieldsMap() []moderation.ShadowCopyField { return []moderation.ShadowCopyField{ - {Key: "PosterID", Value: strconv.FormatInt(cd.PosterID, 10)}, + {Key: "Poster", Value: strconv.FormatInt(cd.PosterID, 10)}, {Key: "IssueID", Value: strconv.FormatInt(cd.IssueID, 10)}, {Key: "Content", Value: cd.Content}, {Key: "ContentVersion", Value: strconv.Itoa(cd.ContentVersion)}, @@ -79,6 +84,11 @@ func (cd CommentData) GetFieldsMap() []moderation.ShadowCopyField { } } +// Implements GetAbuserID() from ShadowCopyData interface, returning the value of PosterID field. +func (cd *CommentData) GetAbuserID() (int64, bool) { + return cd.PosterID, true +} + // newCommentData creates a trimmed down comment to be used just to create a JSON structure // (keeping only the fields relevant for moderation purposes) func newCommentData(comment *Comment) CommentData { diff --git a/models/issues/moderation_test.go b/models/issues/moderation_test.go index adb07bd63a..975712bf2c 100644 --- a/models/issues/moderation_test.go +++ b/models/issues/moderation_test.go @@ -39,7 +39,7 @@ func TestIssueDataGetFieldsMap(t *testing.T) { if assert.Len(t, scFields, 8) { testShadowCopyField(t, scFields[0], "RepoID", "2001") testShadowCopyField(t, scFields[1], "Index", "2") - testShadowCopyField(t, scFields[2], "PosterID", "1002") + testShadowCopyField(t, scFields[2], "Poster", "1002") testShadowCopyField(t, scFields[3], "Title", "Professional marketing services") testShadowCopyField(t, scFields[4], "Content", "Visit my website at promote-your-business.biz for a list of available services.") testShadowCopyField(t, scFields[5], "ContentVersion", "0") @@ -60,7 +60,7 @@ func TestCommentDataGetFieldsMap(t *testing.T) { scFields := cd.GetFieldsMap() if assert.Len(t, scFields, 6) { - testShadowCopyField(t, scFields[0], "PosterID", "1002") + testShadowCopyField(t, scFields[0], "Poster", "1002") testShadowCopyField(t, scFields[1], "IssueID", "3001") testShadowCopyField(t, scFields[2], "Content", "Check out [alexsmith/website](/alexsmith/website)") testShadowCopyField(t, scFields[3], "ContentVersion", "0") diff --git a/models/moderation/abuse_report_detailed.go b/models/moderation/abuse_report_detailed.go index 3d6a6fc4a4..872182a491 100644 --- a/models/moderation/abuse_report_detailed.go +++ b/models/moderation/abuse_report_detailed.go @@ -22,6 +22,11 @@ type AbuseReportDetailed struct { ContentReference string ShadowCopyDate timeutil.TimeStamp // only for details ShadowCopyRawValue string // only for details + + // In case the reported content was deleted before the report was handled, this flag can be set + // in order to try to determine the abuser/poster ID (and load their details) based on the fields + // that might have been saved within the shadow copy (for comments and issues/PRs). + ShouldGetAbuserFromShadowCopy bool `xorm:"-"` } func (ard AbuseReportDetailed) ContentTypeIconName() string { diff --git a/models/moderation/shadow_copy.go b/models/moderation/shadow_copy.go index 8abb32e8ec..05b0bc480c 100644 --- a/models/moderation/shadow_copy.go +++ b/models/moderation/shadow_copy.go @@ -40,6 +40,13 @@ type ShadowCopyData interface { // GetFieldsMap returns a list of pairs with the fields stored within shadow copies // of content reported as abusive, to be used when rendering a shadow copy in the admin UI. GetFieldsMap() []ShadowCopyField + + // GetAbuserID returns the ID of the user who posted the reported content when this info in available within + // the shadow copy (i.e. the PosterID field for comments and issues/PRs or the OwnerID field for repositories), + // together with a boolean value indicating whether the ID is considered valid or not. + // This is used to retrieve the abuser/poster in case the reported content was deleted before an admin managed + // to review the report, allowing them to easily access the abuser profile (if this was not also deleted). + GetAbuserID() (int64, bool) } func init() { diff --git a/models/repo/moderation.go b/models/repo/moderation.go index 0d2672227b..c13654cd48 100644 --- a/models/repo/moderation.go +++ b/models/repo/moderation.go @@ -43,6 +43,11 @@ func (rd RepositoryData) GetFieldsMap() []moderation.ShadowCopyField { } } +// Implements GetAbuserID() from ShadowCopyData interface, returning the value of OwnerID field. +func (rd *RepositoryData) GetAbuserID() (int64, bool) { + return rd.OwnerID, true +} + // newRepositoryData creates a trimmed down repository to be used just to create a JSON structure // (keeping only the fields relevant for moderation purposes) func newRepositoryData(repo *Repository) RepositoryData { diff --git a/models/user/moderation.go b/models/user/moderation.go index 17901f84ec..7bc857489a 100644 --- a/models/user/moderation.go +++ b/models/user/moderation.go @@ -57,6 +57,12 @@ func (ud UserData) GetFieldsMap() []moderation.ShadowCopyField { } } +// Implements GetAbuserID() from ShadowCopyData interface, returning (GhostUserID, false), since for users/organizations +// the ID is not saved within the shadow copy (because it is already stored as ContentID in the abuse report). +func (ud *UserData) GetAbuserID() (int64, bool) { + return GhostUserID, false +} + // newUserData creates a trimmed down user to be used just to create a JSON structure // (keeping only the fields relevant for moderation purposes) func newUserData(user *User) UserData { diff --git a/routers/web/admin/reports.go b/routers/web/admin/reports.go index 1d4cb092e3..862431d8df 100644 --- a/routers/web/admin/reports.go +++ b/routers/web/admin/reports.go @@ -93,6 +93,9 @@ func AbuseReportDetails(ctx *context.Context) { if err = setReportedContentDetails(ctx, reports[0]); err != nil { if user.IsErrUserNotExist(err) || issues.IsErrCommentNotExist(err) || issues.IsErrIssueNotExist(err) || repo_model.IsErrRepoNotExist(err) { ctx.Data["ContentReference"] = ctx.Tr("admin.moderation.deleted_content_ref", reports[0].ContentType, reports[0].ContentID) + if contentType == moderation.ReportedContentTypeComment || contentType == moderation.ReportedContentTypeIssue { + reports[0].ShouldGetAbuserFromShadowCopy = true + } } else { ctx.ServerError("Failed to load reported content details", err) return @@ -103,11 +106,12 @@ func AbuseReportDetails(ctx *context.Context) { } // setReportedContentDetails adds some values into context data for the given report -// (icon name, a reference, the URL and in case of issues and comments also the poster name). +// (icon name, a reference, the URL and in case of issues and comments also the poster name and URL). func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseReportDetailed) error { contentReference := "" var contentURL string var poster string + var posterURL string contentType := report.ContentType contentID := report.ContentID @@ -143,6 +147,7 @@ func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseRep } if issue.Poster != nil { poster = issue.Poster.Name + posterURL = issue.Poster.HomeLink() } contentReference = fmt.Sprintf("%s#%d", issue.Repo.FullName(), issue.Index) @@ -163,6 +168,7 @@ func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseRep } if comment.Poster != nil { poster = comment.Poster.Name + posterURL = comment.Poster.HomeLink() } contentURL = comment.Link(ctx) @@ -172,6 +178,7 @@ func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseRep ctx.Data["ContentReference"] = contentReference ctx.Data["ContentURL"] = contentURL ctx.Data["Poster"] = poster + ctx.Data["PosterURL"] = posterURL return nil } diff --git a/services/moderation/moderating.go b/services/moderation/moderating.go index a8970cd08d..0f9a60caeb 100644 --- a/services/moderation/moderating.go +++ b/services/moderation/moderating.go @@ -61,6 +61,9 @@ func (ca ContentAction) IsValid() bool { // GetShadowCopyMap unmarshals the shadow copy raw value of the given abuse report and returns a list of pairs // (to be rendered when the report is reviewed by an admin). +// It also checks whether the ShouldGetAbuserFromShadowCopy runtime flag is set on the report and if so will try to +// retrieve the abusive user (when their ID can be found within the shadow copy) in order to set some details +// (name and profile link) as context data. // If the report does not have a shadow copy ID or the raw value is empty, returns nil. // If the unmarshal fails a warning is added in the logs and returns nil. func GetShadowCopyMap(ctx *context.Context, ard *moderation.AbuseReportDetailed) []moderation.ShadowCopyField { @@ -81,7 +84,25 @@ func GetShadowCopyMap(ctx *context.Context, ard *moderation.AbuseReportDetailed) log.Warn("Unmarshal failed for shadow copy #%d. %v", ard.ShadowCopyID.Int64, err) return nil } + + if ard.ShouldGetAbuserFromShadowCopy { + abuserID, isValidID := data.GetAbuserID() + if isValidID { + setAbuserDetails(ctx, abuserID) + } + } + return data.GetFieldsMap() } return nil } + +// setAbuserDetails tries to retrieve a user with the given ID and in case +// a user is found it will set their name and profile URL into ctx.Data. +func setAbuserDetails(ctx *context.Context, abuserID int64) { + abuser, err := user.GetPossibleUserByID(ctx, abuserID) + if err == nil { + ctx.Data["AbuserName"] = abuser.Name + ctx.Data["AbuserURL"] = abuser.HomeLink() + } +} diff --git a/templates/admin/moderation/report_details.tmpl b/templates/admin/moderation/report_details.tmpl index 26a8b5964b..976dd95de7 100644 --- a/templates/admin/moderation/report_details.tmpl +++ b/templates/admin/moderation/report_details.tmpl @@ -12,7 +12,7 @@
{{if .ContentURL}}{{.ContentReference}}{{else}}{{.ContentReference}}{{end}} - {{if .Poster}} — {{.Poster}}{{end}} + {{if .Poster}} — {{if .PosterURL}}{{.Poster}}{{else}}{{.Poster}}{{end}}{{end}}
@@ -47,7 +47,7 @@ {{range $scField := (call $.GetShadowCopyMap $.Context .)}} {{$scField.Key}} - {{$scField.Value}} + {{$scField.Value}}{{if and $.AbuserURL (eq $scField.Key "Poster")}} ({{$.AbuserName}}){{end}} {{end}} diff --git a/tests/integration/admin_moderation_test.go b/tests/integration/admin_moderation_test.go index da7beeaabe..7007a5a821 100644 --- a/tests/integration/admin_moderation_test.go +++ b/tests/integration/admin_moderation_test.go @@ -87,9 +87,9 @@ func TestAdminModerationViewReports(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) // Check how many reports are being displayed. - // Reports linked to the same content (type and id) should be grouped; therefore we should see only 8 instead of 11. + // Reports linked to the same content (type and id) should be grouped; therefore we should see only 11 instead of 14. reports := htmlDoc.Find(".admin-setting-content .flex-list .flex-item.report") - assert.Equal(t, 8, reports.Length()) + assert.Equal(t, 11, reports.Length()) // Check details for shown reports. testReportDetails(t, htmlDoc, "1", "octicon-person", "@SPAM-services", "/SPAM-services", "Illegal content", "1") @@ -102,17 +102,24 @@ func TestAdminModerationViewReports(t *testing.T) { testReportDetails(t, htmlDoc, "8", "octicon-issue-opened", "contributor/first#1", "/contributor/first/issues/1", "Other violations of platform rules", "1") // #10 is for a Ghost user testReportDetails(t, htmlDoc, "10", "octicon-person", "Reported content with type 1 and id 9999 no longer exists", "", "Other violations of platform rules", "1") - // #11 if for a comment who's poster was deleted + // #11 is for a comment who's poster was deleted testReportDetails(t, htmlDoc, "11", "octicon-comment", "contributor/first/issues/1#issuecomment-1003", "/contributor/first/issues/1#issuecomment-1003", "Spam", "1") + // #12 is for a comment that was deleted but its poster still exists + testReportDetails(t, htmlDoc, "12", "octicon-comment", "Reported content with type 4 and id 9991 no longer exists", "", "Other violations of platform rules", "1") + // #13 is for a comment that was deleted and the poster was also deleted + testReportDetails(t, htmlDoc, "13", "octicon-comment", "Reported content with type 4 and id 9992 no longer exists", "", "Spam", "1") + // #14 is for a issue that was deleted but its poster still exists + testReportDetails(t, htmlDoc, "14", "octicon-issue-opened", "Reported content with type 3 and id 9991 no longer exists", "", "Spam", "1") t.Run("reports details page", func(t *testing.T) { defer tests.PrintCurrentTest(t)() + // ## There are 3 open reports linked to user 1002. req = NewRequest(t, "GET", "/admin/moderation/reports/type/1/id/1002") resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) - // Check the title (content reference) and corresponding URL + // Check the title (content reference) and corresponding URL. title := htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a") assert.Equal(t, 1, title.Length()) assert.Equal(t, "spammer01", title.Text()) @@ -124,7 +131,7 @@ func TestAdminModerationViewReports(t *testing.T) { reports = htmlDoc.Find(".admin-setting-content .flex-list .flex-item") assert.Equal(t, 3, reports.Length()) - // Poster of comment 1003 was deleted; make sure the details page is still rendered correctly. + // ## Poster of comment 1003 was deleted; make sure the details page is still rendered correctly. req = NewRequest(t, "GET", "/admin/moderation/reports/type/4/id/1003") resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) @@ -136,6 +143,92 @@ func TestAdminModerationViewReports(t *testing.T) { href, exists = title.Attr("href") assert.True(t, exists) assert.Equal(t, "/contributor/first/issues/1#issuecomment-1003", href) + + // ## Comment 9991 was deleted but its poster still exists. + req = NewRequest(t, "GET", "/admin/moderation/reports/type/4/id/9991") + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + + // Check the title (content reference); no URL in case of deleted content. + title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a") + assert.Equal(t, 0, title.Length()) // no anchor because the reported content was deleted + title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title em") + assert.Equal(t, 1, title.Length()) // a generic emphasised title is shown for deleted content + assert.Equal(t, "Reported content with type 4 and id 9991 no longer exists", title.Text()) + + // Check shadow copies for deleted comment when poster still exists. + reports := htmlDoc.Find(".admin-setting-content .flex-list .flex-item") + assert.Equal(t, 1, reports.Length()) // there is only one report for this content + shadowCopyRows := htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr") + assert.Equal(t, 6, shadowCopyRows.Length()) // a shadow copy was created and it has 6 fields + posterRowCells := htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td") + assert.Equal(t, 2, posterRowCells.Length()) + assert.Equal(t, "Poster", posterRowCells.Nodes[0].FirstChild.Data) // in case of comment shadow copies, the first field should be 'Poster' + // For reports of deleted comments, in case the poster still exists + // the value of 'Poster' field should contain a link to the poster profile. + poster := htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td:last-of-type a") + assert.Equal(t, 1, poster.Length()) + assert.Equal(t, "grace", poster.Text()) + href, exists = poster.Attr("href") + assert.True(t, exists) + assert.Equal(t, "/grace", href) + + // ## Comment 9992 was deleted and its poster was also deleted. + req = NewRequest(t, "GET", "/admin/moderation/reports/type/4/id/9992") + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + + // Check the title (content reference); no URL in case of deleted content. + title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a") + assert.Equal(t, 0, title.Length()) // no anchor because the reported content was deleted + title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title em") + assert.Equal(t, 1, title.Length()) // a generic emphasised title is shown for deleted content + assert.Equal(t, "Reported content with type 4 and id 9992 no longer exists", title.Text()) + + // Check shadow copies for deleted comment when poster was also deleted. + reports = htmlDoc.Find(".admin-setting-content .flex-list .flex-item") + assert.Equal(t, 1, reports.Length()) // there is only one report for this content + shadowCopyRows = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr") + assert.Equal(t, 6, shadowCopyRows.Length()) // a shadow copy was created and it has 6 fields + posterRowCells = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td") + assert.Equal(t, 2, posterRowCells.Length()) + assert.Equal(t, "Poster", posterRowCells.Nodes[0].FirstChild.Data) // in case of comment shadow copies, the first field should be 'Poster' + // For reports of deleted comments, in case the poster was also deleted + // the value of 'Poster' field cannot contain a link to the poster profile but only their ID. + poster = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td:last-of-type a") + assert.Equal(t, 0, poster.Length()) + poster = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td:last-of-type") + assert.Equal(t, 1, poster.Length()) + assert.Equal(t, "9999", poster.Text()) + + // ## Issue 9991 was deleted but its poster still exists. + req = NewRequest(t, "GET", "/admin/moderation/reports/type/3/id/9991") + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + + // Check the title (content reference); no URL in case of deleted content. + title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a") + assert.Equal(t, 0, title.Length()) // no anchor because the reported content was deleted + title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title em") + assert.Equal(t, 1, title.Length()) // a generic emphasised title is shown for deleted content + assert.Equal(t, "Reported content with type 3 and id 9991 no longer exists", title.Text()) + + // Check shadow copies for deleted issue when poster still exists. + reports = htmlDoc.Find(".admin-setting-content .flex-list .flex-item") + assert.Equal(t, 1, reports.Length()) // there is only one report for this content + shadowCopyRows = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr") + assert.Equal(t, 8, shadowCopyRows.Length()) // a shadow copy was created and it has 8 fields + posterRowCells = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:nth-of-type(3) td") + assert.Equal(t, 2, posterRowCells.Length()) + assert.Equal(t, "Poster", posterRowCells.Nodes[0].FirstChild.Data) // in case of issue shadow copies, the third field should be 'Poster' + // For reports of deleted issues, in case the poster still exists + // the value of 'Poster' field should contain a link to the poster profile. + poster = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:nth-of-type(3) td:last-of-type a") + assert.Equal(t, 1, poster.Length()) + assert.Equal(t, "frank", poster.Text()) + href, exists = poster.Attr("href") + assert.True(t, exists) + assert.Equal(t, "/frank", href) }) }) }) diff --git a/tests/integration/fixtures/TestAdminModerationViewReports/abuse_report.yml b/tests/integration/fixtures/TestAdminModerationViewReports/abuse_report.yml index 14d7b76155..8d4862f7ee 100644 --- a/tests/integration/fixtures/TestAdminModerationViewReports/abuse_report.yml +++ b/tests/integration/fixtures/TestAdminModerationViewReports/abuse_report.yml @@ -120,3 +120,36 @@ remarks: This user just posted a spammy comment on my issue. shadow_copy_id: null created_unix: 1752697820 # 2025-07-16 20:30:20 + +- # Report of a comment that was afterwards deleted (and a shadow copy was created) but the poster still exists. + id: 12 + status: 1 + reporter_id: 1001 # @contributor + content_type: 4 # Comment + content_id: 9991 # deleted comment + category: 1 # Other + remarks: Accuses me of being a frog. + shadow_copy_id: 1201 + created_unix: 1752748200 # 2005-07-17 10:30:00 + +- # Report of a comment that was afterwards deleted (and a shadow copy was created) and the poster was also deleted. + id: 13 + status: 1 + reporter_id: 1004 # @reporter1 + content_type: 4 # Comment + content_id: 9992 # deleted comment + category: 2 # Spam + remarks: This comment advertises a product. + shadow_copy_id: 1301 + created_unix: 1752751260 # 2005-07-17 11:21:00 + +- # Report of an issue that was afterwards deleted (and a shadow copy was created) but the poster still exists. + id: 14 + status: 1 + reporter_id: 1004 # @reporter1 + content_type: 3 # Issue (issues or pull requests) + content_id: 9991 # deleted issue + category: 2 # Spam + remarks: This issue was created for advertising a business. + shadow_copy_id: 1401 + created_unix: 1752751320 # 2005-07-17 11:22:00 diff --git a/tests/integration/fixtures/TestAdminModerationViewReports/abuse_report_shadow_copy.yml b/tests/integration/fixtures/TestAdminModerationViewReports/abuse_report_shadow_copy.yml new file mode 100644 index 0000000000..9b95ce8fe1 --- /dev/null +++ b/tests/integration/fixtures/TestAdminModerationViewReports/abuse_report_shadow_copy.yml @@ -0,0 +1,14 @@ +- + id: 1201 + raw_value: '{"PosterID":1102,"IssueID":1001,"Content":"You are just a frog.","ContentVersion":0,"CreatedUnix":1752747600,"UpdatedUnix":1752747600}' + created_unix: 1752748260 # 2025-07-17 10:31:00 + +- + id: 1301 + raw_value: '{"PosterID":9999,"IssueID":1001,"Content":"Best cleaning products on http://cleaning.test","ContentVersion":0,"CreatedUnix":1752750660,"UpdatedUnix":1752750660}' + created_unix: 1752751860 # 2025-07-17 11:31:00 + +- + id: 1401 + raw_value: '{"RepoID":1001,"Index":9,"PosterID":1101,"Title":"Professional cleaning products","Content":"We provide a wide range of cleaning products. Visit [our website](https://doubtfulshops.test/cleaning) for more details.","ContentVersion":0,"CreatedUnix":1752750720,"UpdatedUnix":1752750720}' + created_unix: 1752751920 # 2025-07-17 11:32:00 diff --git a/tests/integration/fixtures/TestAdminModerationViewReports/user.yml b/tests/integration/fixtures/TestAdminModerationViewReports/user.yml index f00f3de338..75639657e8 100644 --- a/tests/integration/fixtures/TestAdminModerationViewReports/user.yml +++ b/tests/integration/fixtures/TestAdminModerationViewReports/user.yml @@ -106,3 +106,44 @@ avatar: avatar-hash-1005 avatar_email: reporter2@example.org use_custom_avatar: false + +- # This is an abusive user (a spammer). + id: 1101 + lower_name: frank + name: frank + full_name: Frank + email: frank@doubtfulshops.test + keep_email_private: false + passwd: passwdSalt:password + passwd_hash_algo: dummy + type: 0 + salt: passwdSalt + website: https://doubtfulshops.test + description: 'We are a startup company which developed a novel cleaning product. Check out our website for latest offers. https://doubtfulshops.test/cleaning' + created_unix: 1752747600 # 2025-07-17 10:20:00 + is_active: true + is_admin: false + is_restricted: false + avatar: avatar-hash-1101 + avatar_email: frank@doubtfulshops.test + use_custom_avatar: false + +- # This is an abusive user (an offender). + id: 1102 + lower_name: grace + name: grace + full_name: Grace + email: grace@example.org + keep_email_private: false + passwd: passwdSalt:password + passwd_hash_algo: dummy + type: 0 + salt: passwdSalt + description: '' + created_unix: 1752747630 # 2025-07-17 10:20:30 + is_active: true + is_admin: false + is_restricted: false + avatar: avatar-hash-1102 + avatar_email: grace@example.org + use_custom_avatar: false