Fix IM report and group permission regressions
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
//go:build mysql
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func TestCanUseManagedReportsRequiresAllOrganizationVisibility(t *testing.T) {
|
||||
if !canUseManagedReports(organizationVisibility{Scope: orgVisibilityAll}) {
|
||||
t.Fatal("all visibility should be allowed to use managed report scope")
|
||||
}
|
||||
for _, scope := range []string{orgVisibilitySelf, orgVisibilityFriends, ""} {
|
||||
if canUseManagedReports(organizationVisibility{Scope: scope}) {
|
||||
t.Fatalf("scope %q should not be allowed to use managed report scope", scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportMemberScopeWhereIncludesDepartmentAndVisibility(t *testing.T) {
|
||||
where, args := reportMemberScopeWhere(organizationVisibility{Scope: orgVisibilityFriends, UserID: 42}, "dep_ops")
|
||||
joined := strings.Join(where, " AND ")
|
||||
if !strings.Contains(joined, "m.department_id=?") {
|
||||
t.Fatalf("department filter missing from where: %s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "im_friendships") {
|
||||
t.Fatalf("friend visibility filter missing from where: %s", joined)
|
||||
}
|
||||
if len(args) != 4 || args[0] != "dep_ops" || args[1] != int64(42) || args[2] != int64(42) || args[3] != types.RelationshipFriend {
|
||||
t.Fatalf("unexpected args: %#v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupAdminAccessModeDoesNotGrantOwner(t *testing.T) {
|
||||
mode := groupAdminAccessMode()
|
||||
if !mode.IsAdmin() {
|
||||
t.Fatalf("admin mode should grant admin access: %s", mode.String())
|
||||
}
|
||||
if mode.IsOwner() {
|
||||
t.Fatalf("admin mode must not grant owner access: %s", mode.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageSearchDoesNotClearScannedHead(t *testing.T) {
|
||||
source, err := os.ReadFile("tenant_adapter.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(source), "msg.Head = kvMapFromJSON(msg.Head)") {
|
||||
t.Fatal("message search must not clear Head after sqlx StructScan has already decoded it")
|
||||
}
|
||||
}
|
||||
@@ -2337,7 +2337,6 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu
|
||||
return nil, "", err
|
||||
}
|
||||
msg := finalizeTenantMessage(id, row.Message, row.RecalledBy)
|
||||
msg.Head = kvMapFromJSON(msg.Head)
|
||||
if len(msgs) == limit {
|
||||
nextCursor = strconv.FormatInt(msg.SeqId, 10)
|
||||
continue
|
||||
@@ -2398,7 +2397,6 @@ func (a *adapter) tenantGroupMessageSearch(id t.TenantID, topic string, uid t.Ui
|
||||
return nil, "", err
|
||||
}
|
||||
msg := normalizeGroupMessage(id, row.Message, row.RecalledBy)
|
||||
msg.Head = kvMapFromJSON(msg.Head)
|
||||
if len(msgs) == limit {
|
||||
nextCursor = strconv.FormatInt(msg.SeqId, 10)
|
||||
continue
|
||||
@@ -4593,12 +4591,24 @@ func (a *adapter) TenantDailyReportList(id t.TenantID, uid t.Uid, scope, from, t
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
where := []string{"1=1"}
|
||||
args := []any{}
|
||||
if scope == "" || scope == "mine" {
|
||||
where = append(where, "author_id=?")
|
||||
args = append(args, decodeUID(uid))
|
||||
} else if scope != "managed" {
|
||||
} else if scope == "managed" {
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if !canUseManagedReports(visibility) {
|
||||
return nil, "", t.ErrPermissionDenied
|
||||
}
|
||||
} else {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if from != "" {
|
||||
@@ -4622,10 +4632,6 @@ func (a *adapter) TenantDailyReportList(id t.TenantID, uid t.Uid, scope, from, t
|
||||
args = append(args, parsed)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT * FROM im_daily_reports WHERE `+strings.Join(where, " AND ")+` ORDER BY id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -4769,14 +4775,14 @@ func (a *adapter) TenantDailyReportSubmit(id t.TenantID, uid t.Uid, reportID str
|
||||
return a.TenantDailyReportGet(id, uid, reportID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantDailyReportStats(id t.TenantID, uid t.Uid, date time.Time, _ string) (*t.DailyReportStats, error) {
|
||||
func (a *adapter) TenantDailyReportStats(id t.TenantID, uid t.Uid, date time.Time, departmentID string) (*t.DailyReportStats, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantDailyReportStats(id, uid, date, "")
|
||||
return tenantAdp.TenantDailyReportStats(id, uid, date, departmentID)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
@@ -4786,11 +4792,22 @@ func (a *adapter) TenantDailyReportStats(id t.TenantID, uid t.Uid, date time.Tim
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
stats := &t.DailyReportStats{}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT user_id) FROM im_org_members WHERE employment_state='active'`).Scan(&stats.ExpectedCount); err != nil {
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM im_daily_reports WHERE report_date=? AND status=?`, date.Format("2006-01-02"), t.DailyReportStatusSubmitted).Scan(&stats.SubmittedCount); err != nil {
|
||||
memberWhere, memberArgs := reportMemberScopeWhere(visibility, departmentID)
|
||||
stats := &t.DailyReportStats{}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT m.user_id) FROM im_org_members AS m WHERE `+strings.Join(memberWhere, " AND "), memberArgs...).Scan(&stats.ExpectedCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submittedWhere := []string{"r.report_date=?", "r.status=?"}
|
||||
submittedArgs := []any{date.Format("2006-01-02"), t.DailyReportStatusSubmitted}
|
||||
if len(memberWhere) > 0 {
|
||||
submittedWhere = append(submittedWhere, `EXISTS (SELECT 1 FROM im_org_members AS m WHERE m.user_id=r.author_id AND `+strings.Join(memberWhere, " AND ")+`)`)
|
||||
submittedArgs = append(submittedArgs, memberArgs...)
|
||||
}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM im_daily_reports AS r WHERE `+strings.Join(submittedWhere, " AND "), submittedArgs...).Scan(&stats.SubmittedCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MissingCount = stats.ExpectedCount - stats.SubmittedCount
|
||||
@@ -4800,20 +4817,114 @@ func (a *adapter) TenantDailyReportStats(id t.TenantID, uid t.Uid, date time.Tim
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantDailyReportRemind(id t.TenantID, uid t.Uid, _ time.Time, userIDs []t.Uid, _ string) ([]t.Uid, error) {
|
||||
func reportMemberScopeWhere(visibility organizationVisibility, departmentID string) ([]string, []any) {
|
||||
memberWhere := []string{"m.employment_state='active'"}
|
||||
memberArgs := []any{}
|
||||
if strings.TrimSpace(departmentID) != "" {
|
||||
memberWhere = append(memberWhere, "m.department_id=?")
|
||||
memberArgs = append(memberArgs, strings.TrimSpace(departmentID))
|
||||
}
|
||||
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
|
||||
if visibilitySQL != "" {
|
||||
memberWhere = append(memberWhere, visibilitySQL)
|
||||
memberArgs = append(memberArgs, visibilityArgs...)
|
||||
}
|
||||
return memberWhere, memberArgs
|
||||
}
|
||||
|
||||
func canUseManagedReports(visibility organizationVisibility) bool {
|
||||
return visibility.Scope == orgVisibilityAll
|
||||
}
|
||||
|
||||
func (a *adapter) TenantDailyReportRemind(id t.TenantID, uid t.Uid, date time.Time, userIDs []t.Uid, idempotencyKey string) ([]t.Uid, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantDailyReportRemind(id, uid, time.Time{}, userIDs, "")
|
||||
return tenantAdp.TenantDailyReportRemind(id, uid, date, userIDs, idempotencyKey)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || date.IsZero() || len(userIDs) == 0 || strings.TrimSpace(idempotencyKey) == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
return userIDs, nil
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !canUseManagedReports(visibility) {
|
||||
return nil, t.ErrPermissionDenied
|
||||
}
|
||||
targetIDs := make([]int64, 0, len(userIDs))
|
||||
seen := map[int64]struct{}{}
|
||||
for _, target := range userIDs {
|
||||
if target.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
decoded := decodeUID(target)
|
||||
if decoded <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if _, ok := seen[decoded]; ok {
|
||||
continue
|
||||
}
|
||||
seen[decoded] = struct{}{}
|
||||
targetIDs = append(targetIDs, decoded)
|
||||
}
|
||||
query, args, err := sqlx.In(`SELECT DISTINCT m.user_id FROM im_org_members AS m
|
||||
WHERE m.employment_state='active' AND m.user_id IN (?) AND NOT EXISTS (
|
||||
SELECT 1 FROM im_daily_reports AS r
|
||||
WHERE r.author_id=m.user_id AND r.report_date=? AND r.status=?
|
||||
)`, targetIDs, date.Format("2006-01-02"), t.DailyReportStatusSubmitted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = a.db.Rebind(query)
|
||||
rows, err := a.db.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
reminded := make([]t.Uid, 0, len(targetIDs))
|
||||
for rows.Next() {
|
||||
var raw int64
|
||||
if err = rows.Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := store.EncodeUid(raw)
|
||||
reminded = append(reminded, target)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
for _, target := range reminded {
|
||||
eventID := t.StableEventID("daily_report_reminder", date.Format("2006-01-02"), target.UserId(), strings.TrimSpace(idempotencyKey))
|
||||
if err = insertSystemNotificationTx(tx, target, eventID, "daily_report.reminded", "日报提醒", date.Format("2006-01-02"), map[string]any{
|
||||
"date": date.Format("2006-01-02"),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
committed = true
|
||||
return reminded, nil
|
||||
}
|
||||
|
||||
func scanDailyReport(rows *sqlx.Rows) (*t.DailyReport, error) {
|
||||
@@ -4869,12 +4980,24 @@ func (a *adapter) TenantMonthlyReportList(id t.TenantID, uid t.Uid, scope, fromM
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
where := []string{"1=1"}
|
||||
args := []any{}
|
||||
if scope == "" || scope == "mine" {
|
||||
where = append(where, "author_id=?")
|
||||
args = append(args, decodeUID(uid))
|
||||
} else if scope != "managed" {
|
||||
} else if scope == "managed" {
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if !canUseManagedReports(visibility) {
|
||||
return nil, "", t.ErrPermissionDenied
|
||||
}
|
||||
} else {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if fromMonth != "" {
|
||||
@@ -4898,10 +5021,6 @@ func (a *adapter) TenantMonthlyReportList(id t.TenantID, uid t.Uid, scope, fromM
|
||||
args = append(args, parsed)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT * FROM im_monthly_reports WHERE `+strings.Join(where, " AND ")+` ORDER BY id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -5076,14 +5195,14 @@ func (a *adapter) TenantMonthlyReportReopen(id t.TenantID, uid t.Uid, reportID s
|
||||
return a.TenantMonthlyReportGet(id, uid, reportID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMonthlyReportStats(id t.TenantID, uid t.Uid, month, _ string) (*t.MonthlyReportStats, error) {
|
||||
func (a *adapter) TenantMonthlyReportStats(id t.TenantID, uid t.Uid, month, departmentID string) (*t.MonthlyReportStats, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantMonthlyReportStats(id, uid, month, "")
|
||||
return tenantAdp.TenantMonthlyReportStats(id, uid, month, departmentID)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
@@ -5093,11 +5212,22 @@ func (a *adapter) TenantMonthlyReportStats(id t.TenantID, uid t.Uid, month, _ st
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
stats := &t.MonthlyReportStats{}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT user_id) FROM im_org_members WHERE employment_state='active'`).Scan(&stats.ExpectedCount); err != nil {
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM im_monthly_reports WHERE report_month=? AND status=?`, month, t.DailyReportStatusSubmitted).Scan(&stats.SubmittedCount); err != nil {
|
||||
memberWhere, memberArgs := reportMemberScopeWhere(visibility, departmentID)
|
||||
stats := &t.MonthlyReportStats{}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT m.user_id) FROM im_org_members AS m WHERE `+strings.Join(memberWhere, " AND "), memberArgs...).Scan(&stats.ExpectedCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submittedWhere := []string{"r.report_month=?", "r.status=?"}
|
||||
submittedArgs := []any{month, t.DailyReportStatusSubmitted}
|
||||
if len(memberWhere) > 0 {
|
||||
submittedWhere = append(submittedWhere, `EXISTS (SELECT 1 FROM im_org_members AS m WHERE m.user_id=r.author_id AND `+strings.Join(memberWhere, " AND ")+`)`)
|
||||
submittedArgs = append(submittedArgs, memberArgs...)
|
||||
}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM im_monthly_reports AS r WHERE `+strings.Join(submittedWhere, " AND "), submittedArgs...).Scan(&stats.SubmittedCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MissingCount = stats.ExpectedCount - stats.SubmittedCount
|
||||
|
||||
@@ -1170,8 +1170,8 @@ func (a *adapter) TenantGroupRoleUpdate(id t.TenantID, topic string, actor, targ
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
res, err := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
modeGiven.String(), now, topic, decodeUID(target))
|
||||
res, err := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,modewant=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
modeGiven.String(), modeGiven.String(), now, topic, decodeUID(target))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1194,6 +1194,10 @@ func (a *adapter) TenantGroupRoleUpdate(id t.TenantID, topic string, actor, targ
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func groupAdminAccessMode() t.AccessMode {
|
||||
return t.ModeCPublic | t.ModeApprove
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupOwnerTransfer(id t.TenantID, topic string, previousOwner, newOwner t.Uid,
|
||||
version int, now time.Time) error {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
@@ -1214,10 +1218,10 @@ func (a *adapter) TenantGroupOwnerTransfer(id t.TenantID, topic string, previous
|
||||
uid t.Uid
|
||||
mode t.AccessMode
|
||||
role string
|
||||
}{{previousOwner, t.ModeCAdmin | t.ModeCPublic, t.GroupMemberRoleAdmin}, {newOwner, t.ModeCFull, t.GroupMemberRoleOwner}}
|
||||
}{{previousOwner, groupAdminAccessMode(), t.GroupMemberRoleAdmin}, {newOwner, t.ModeCFull, t.GroupMemberRoleOwner}}
|
||||
for _, update := range updates {
|
||||
res, execErr := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
update.mode.String(), now, topic, decodeUID(update.uid))
|
||||
res, execErr := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,modewant=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
update.mode.String(), update.mode.String(), now, topic, decodeUID(update.uid))
|
||||
if execErr != nil {
|
||||
return execErr
|
||||
}
|
||||
|
||||
+5
-3
@@ -2381,8 +2381,8 @@ func serveIMDailyReports(wrt http.ResponseWriter, req *http.Request) {
|
||||
"permissions": map[string]any{
|
||||
"can_create": true,
|
||||
"makeup_allowed_days": 7,
|
||||
"can_view_managed": true,
|
||||
"can_remind_unsubmitted": true,
|
||||
"can_view_managed": false,
|
||||
"can_remind_unsubmitted": false,
|
||||
},
|
||||
})
|
||||
case http.MethodPost:
|
||||
@@ -3882,6 +3882,7 @@ func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
return
|
||||
}
|
||||
topic = normalizeConversationSettingsTopic(topic, principal.Uid)
|
||||
var body imMessageRecallRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) || body.Version <= 0 {
|
||||
if body.Version <= 0 {
|
||||
@@ -3933,6 +3934,7 @@ func serveIMMessageRevision(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
return
|
||||
}
|
||||
topic = normalizeConversationSettingsTopic(topic, principal.Uid)
|
||||
var body imMessageRevisionRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
@@ -5294,7 +5296,7 @@ func serveIMGroupAdmins(wrt http.ResponseWriter, req *http.Request, topic, tail
|
||||
}
|
||||
item["user_id"] = uid.UserId()
|
||||
if err := store.GroupBusiness.UpdateRole(principal.TenantID, topic, principal.Uid, uid,
|
||||
types.GroupMemberRoleAdmin, types.ModeCAdmin|types.ModeCPublic, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
types.GroupMemberRoleAdmin, types.ModeCPublic|types.ModeApprove, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
item["ok"] = false
|
||||
item["reason"] = relationErrorReason(err)
|
||||
} else {
|
||||
|
||||
+73
-5
@@ -333,6 +333,7 @@ type testGroupBusinessStore struct {
|
||||
invitations []types.GroupInvitation
|
||||
updatedSettings types.GroupSettingsPatch
|
||||
updatedMembers []types.GroupMemberPatch
|
||||
updatedRoleMode types.AccessMode
|
||||
}
|
||||
|
||||
func (s *testGroupBusinessStore) CreateGroup(_ types.TenantID, _ *types.Topic, _ types.Uid, _ any,
|
||||
@@ -517,8 +518,9 @@ func (s *testGroupBusinessStore) Leave(_ types.TenantID, _ string, _ types.Uid,
|
||||
}
|
||||
|
||||
func (s *testGroupBusinessStore) UpdateRole(_ types.TenantID, topic string, _ types.Uid, target types.Uid,
|
||||
role string, _ types.AccessMode, _ time.Time) error {
|
||||
role string, mode types.AccessMode, _ time.Time) error {
|
||||
s.updatedMembers = append(s.updatedMembers, types.GroupMemberPatch{Role: &role})
|
||||
s.updatedRoleMode = mode
|
||||
s.member = &types.GroupMember{Topic: topic, UserID: target, Role: role, MemberStatus: types.GroupMemberStatusActive}
|
||||
return nil
|
||||
}
|
||||
@@ -3692,7 +3694,8 @@ func TestServeIMGroupDetailPatchSettingsProfileAndLeave(t *testing.T) {
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
store.Topics = topics
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = &testGroupBusinessStore{}
|
||||
groupStore := &testGroupBusinessStore{}
|
||||
store.GroupBusiness = groupStore
|
||||
defer func() {
|
||||
store.Store = oldStore
|
||||
store.Topics = oldTopics
|
||||
@@ -3771,7 +3774,8 @@ func TestServeIMGroupLeave(t *testing.T) {
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
store.Topics = topics
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = &testGroupBusinessStore{}
|
||||
groupStore := &testGroupBusinessStore{}
|
||||
store.GroupBusiness = groupStore
|
||||
defer func() {
|
||||
store.Topics = oldTopics
|
||||
store.Subs = oldSubs
|
||||
@@ -4020,7 +4024,8 @@ func TestServeIMGroupAnnouncementGovernanceJoinRequest(t *testing.T) {
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
store.Topics = topics
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = &testGroupBusinessStore{}
|
||||
groupStore := &testGroupBusinessStore{}
|
||||
store.GroupBusiness = groupStore
|
||||
defer func() {
|
||||
store.Store = oldStore
|
||||
store.Topics = oldTopics
|
||||
@@ -4083,7 +4088,8 @@ func TestServeIMGroupAdminsMutesAndOwnerTransfer(t *testing.T) {
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
store.Topics = topics
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = &testGroupBusinessStore{}
|
||||
groupStore := &testGroupBusinessStore{}
|
||||
store.GroupBusiness = groupStore
|
||||
defer func() {
|
||||
store.Store = oldStore
|
||||
store.Topics = oldTopics
|
||||
@@ -4106,6 +4112,9 @@ func TestServeIMGroupAdminsMutesAndOwnerTransfer(t *testing.T) {
|
||||
if adminRec.Code != http.StatusOK {
|
||||
t.Fatalf("admin status: got %d body=%s", adminRec.Code, adminRec.Body.String())
|
||||
}
|
||||
if !groupStore.updatedRoleMode.IsAdmin() || groupStore.updatedRoleMode.IsOwner() {
|
||||
t.Fatalf("admin role mode should grant admin without owner: %s", groupStore.updatedRoleMode.String())
|
||||
}
|
||||
|
||||
muteReq := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/mutes", bytes.NewBufferString(`{"members":[{"user_id":"`+target.UserId()+`"}],"reason":"test"}`))
|
||||
muteReq.Header.Set("Authorization", "Bearer token-1")
|
||||
@@ -5165,6 +5174,65 @@ func TestServeIMMessageRecall(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMessageRecallNormalizesP2PUserTopic(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
actor := types.Uid(42)
|
||||
peer := types.Uid(43)
|
||||
normalizedTopic := actor.P2PName(peer)
|
||||
expectIMBearer(t, ctrl, "token-1", 7, actor)
|
||||
oldSubs := store.Subs
|
||||
oldMessages := store.Messages
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.Messages = messages
|
||||
defer func() {
|
||||
store.Subs = oldSubs
|
||||
store.Messages = oldMessages
|
||||
}()
|
||||
|
||||
subs.EXPECT().Get(types.TenantID(7), normalizedTopic, actor, false).Return(&types.Subscription{
|
||||
TenantID: 7,
|
||||
User: actor.String(),
|
||||
Topic: normalizedTopic,
|
||||
ModeWant: types.ModeCPublic,
|
||||
ModeGiven: types.ModeCPublic,
|
||||
}, nil)
|
||||
messages.EXPECT().GetOne(types.TenantID(7), normalizedTopic, int64(9)).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
SeqId: 9,
|
||||
Topic: normalizedTopic,
|
||||
From: actor.String(),
|
||||
Version: 1,
|
||||
ObjHeader: types.ObjHeader{CreatedAt: time.Now().Add(-time.Second)},
|
||||
}, nil)
|
||||
messages.EXPECT().Recall(types.TenantID(7), normalizedTopic, int64(9), actor, 1, gomock.Any()).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
SeqId: 9,
|
||||
Topic: normalizedTopic,
|
||||
From: actor.String(),
|
||||
Version: 2,
|
||||
RecalledAt: ptrTime(time.Now().UTC()),
|
||||
RecalledBy: actor.String(),
|
||||
ObjHeader: types.ObjHeader{UpdatedAt: time.Now().UTC()},
|
||||
}, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/topics/"+peer.UserId()+"/messages/9/recall", bytes.NewBufferString(`{"version":1}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMMessageRecall(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["topic"] != normalizedTopic {
|
||||
t.Fatalf("expected normalized topic %q, got %#v", normalizedTopic, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMessageRevision(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
Reference in New Issue
Block a user