Ctrl K
ComunidadGuías…

internal/store/postgres/channel_admin_integration_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"errors"
	"telesrv/internal/domain"
	"testing"
)

func TestChannelStoreEditAboutPersistsAndChecksPermission(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)

	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{
		AccessHash: 131,
		Phone:      "+1888" + suffix + "01",
		FirstName:  "AboutOwner",
	})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	member, err := users.Create(ctx, domain.User{
		AccessHash: 132,
		Phone:      "+1888" + suffix + "02",
		FirstName:  "AboutMember",
	})
	if err != nil {
		t.Fatalf("create member: %v", err)
	}
	var channelID int64
	t.Cleanup(func() {
		if channelID != 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
	})

	channels := NewChannelStore(pool)
	created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "About " + suffix,
		Megagroup:     true,
		MemberUserIDs: []int64{member.ID},
		Date:          1700000600,
	})
	if err != nil {
		t.Fatalf("create channel: %v", err)
	}
	channelID = created.Channel.ID

	if _, err := channels.EditChannelAbout(ctx, domain.EditChannelAboutRequest{
		UserID:    member.ID,
		ChannelID: channelID,
		About:     "member cannot edit",
		Date:      1700000601,
	}); !errors.Is(err, domain.ErrChannelAdminRequired) {
		t.Fatalf("EditChannelAbout by member err = %v, want ErrChannelAdminRequired", err)
	}

	updated, err := channels.EditChannelAbout(ctx, domain.EditChannelAboutRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		About:     "owner about",
		Date:      1700000602,
	})
	if err != nil {
		t.Fatalf("EditChannelAbout by owner: %v", err)
	}
	if updated.About != "owner about" {
		t.Fatalf("updated about = %q, want owner about", updated.About)
	}
	view, err := channels.GetChannel(ctx, member.ID, channelID)
	if err != nil {
		t.Fatalf("GetChannel by member: %v", err)
	}
	if view.Channel.About != "owner about" {
		t.Fatalf("member view about = %q, want owner about", view.Channel.About)
	}
}

func TestChannelStoreCreatorCanEditOwnAdminRights(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)
	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{
		AccessHash: 141,
		Phone:      "+1888" + suffix + "11",
		FirstName:  "CreatorSelfOwner",
	})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	admin, err := users.Create(ctx, domain.User{
		AccessHash: 142,
		Phone:      "+1888" + suffix + "12",
		FirstName:  "CreatorSelfAdmin",
	})
	if err != nil {
		t.Fatalf("create admin: %v", err)
	}
	var channelID int64
	t.Cleanup(func() {
		if channelID != 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, admin.ID})
	})

	channels := NewChannelStore(pool)
	created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "Creator Self " + suffix,
		Megagroup:     true,
		MemberUserIDs: []int64{admin.ID},
		Date:          1700000610,
	})
	if err != nil {
		t.Fatalf("create channel: %v", err)
	}
	channelID = created.Channel.ID

	edited, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		MemberID:  owner.ID,
		AdminRights: domain.ChannelAdminRights{
			Anonymous: true,
		},
		Date: 1700000611,
	})
	if err != nil {
		t.Fatalf("creator edits own admin rights: %v", err)
	}
	if edited.Participant.Role != domain.ChannelRoleCreator || !edited.Participant.AdminRights.Anonymous || !edited.Participant.AdminRights.ChangeInfo || !edited.Participant.AdminRights.AddAdmins {
		t.Fatalf("edited creator = %+v, want creator with anonymous plus full creator projection", edited.Participant)
	}
	persisted, err := channels.GetParticipant(ctx, owner.ID, channelID, owner.ID)
	if err != nil {
		t.Fatalf("get creator participant: %v", err)
	}
	if persisted.Role != domain.ChannelRoleCreator || !persisted.AdminRights.Anonymous || !persisted.AdminRights.ChangeInfo || !persisted.AdminRights.AddAdmins {
		t.Fatalf("persisted creator = %+v, want creator with anonymous plus full creator projection", persisted)
	}

	if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		MemberID:  admin.ID,
		AdminRights: domain.ChannelAdminRights{
			ChangeInfo: true,
			AddAdmins:  true,
		},
		Date: 1700000612,
	}); err != nil {
		t.Fatalf("promote admin: %v", err)
	}
	if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		UserID:    admin.ID,
		ChannelID: channelID,
		MemberID:  owner.ID,
		AdminRights: domain.ChannelAdminRights{
			ChangeInfo: true,
		},
		Date: 1700000613,
	}); !errors.Is(err, domain.ErrChannelUserCreator) {
		t.Fatalf("admin edits creator err = %v, want ErrChannelUserCreator", err)
	}
}

func TestChannelStoreAdminLogFiltersAndSearch(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)

	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{
		AccessHash: 51,
		Phone:      "+1999" + suffix + "01",
		FirstName:  "AdminLogOwner",
	})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	friend, err := users.Create(ctx, domain.User{
		AccessHash: 52,
		Phone:      "+1999" + suffix + "02",
		FirstName:  "AdminLogFriend",
	})
	if err != nil {
		t.Fatalf("create friend: %v", err)
	}
	invited, err := users.Create(ctx, domain.User{
		AccessHash: 53,
		Phone:      "+1999" + suffix + "03",
		FirstName:  "AdminLogInvited",
	})
	if err != nil {
		t.Fatalf("create invited: %v", err)
	}
	var channelID int64
	t.Cleanup(func() {
		if channelID != 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID, invited.ID})
	})

	channels := NewChannelStore(pool)
	created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "Admin Log " + suffix,
		Megagroup:     true,
		MemberUserIDs: []int64{friend.ID},
		Date:          1700000500,
	})
	if err != nil {
		t.Fatalf("create channel: %v", err)
	}
	channelID = created.Channel.ID

	if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		MemberID:  friend.ID,
		AdminRights: domain.ChannelAdminRights{
			ChangeInfo:  true,
			InviteUsers: true,
			PinMessages: true,
		},
		Rank: "ops",
		Date: 1700000501,
	}); err != nil {
		t.Fatalf("edit admin: %v", err)
	}
	sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		RandomID:  501,
		Message:   "needle admin log body",
		Date:      1700000502,
	})
	if err != nil {
		t.Fatalf("send channel message: %v", err)
	}
	if _, err := channels.UpdatePinnedMessage(ctx, domain.UpdateChannelPinnedMessageRequest{
		UserID:    friend.ID,
		ChannelID: channelID,
		MessageID: sent.Message.ID,
		Pinned:    true,
		Date:      1700000503,
	}); err != nil {
		t.Fatalf("pin message: %v", err)
	}
	if _, err := channels.InviteToChannel(ctx, channelID, friend.ID, []int64{invited.ID}, 1700000504); err != nil {
		t.Fatalf("invite to channel: %v", err)
	}

	searched, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		Query:     "needle",
		Limit:     10,
	})
	if err != nil {
		t.Fatalf("search admin log: %v", err)
	}
	if len(searched.Events) == 0 {
		t.Fatalf("search admin log returned no events, want message body match")
	}

	pinned, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		Filter:    domain.ChannelAdminLogFilter{Pinned: true},
		Limit:     10,
	})
	if err != nil {
		t.Fatalf("pinned admin log: %v", err)
	}
	if len(pinned.Events) != 1 || pinned.Events[0].Type != domain.ChannelAdminLogUpdatePinned || pinned.Events[0].Message == nil {
		t.Fatalf("pinned events = %+v, want one update_pinned with message", pinned.Events)
	}

	byFriend, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
		UserID:       owner.ID,
		ChannelID:    channelID,
		AdminUserIDs: []int64{friend.ID},
		Limit:        10,
	})
	if err != nil {
		t.Fatalf("friend admin log: %v", err)
	}
	if len(byFriend.Events) == 0 {
		t.Fatalf("friend admin log returned no events, want pin/invite")
	}
	for _, event := range byFriend.Events {
		if event.UserID != friend.ID {
			t.Fatalf("friend admin log event actor = %d, want %d in %+v", event.UserID, friend.ID, byFriend.Events)
		}
	}

	if _, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
		UserID:    invited.ID,
		ChannelID: channelID,
		Limit:     10,
	}); err != domain.ErrChannelAdminRequired {
		t.Fatalf("member admin log err = %v, want ErrChannelAdminRequired", err)
	}
}

// TestChannelStoreListSendAsChannelsPostgres exercises the postgres ListSendAsChannels query against a
// real database: creator-owned and PostMessages-admin broadcast channels are included; an admin without
// PostMessages, an owned megagroup, and a member-only broadcast are excluded.
func TestChannelStoreListSendAsChannelsPostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)

	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{AccessHash: 211, Phone: "+1444" + suffix + "01", FirstName: "SendAsOwner"})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	host, err := users.Create(ctx, domain.User{AccessHash: 212, Phone: "+1444" + suffix + "02", FirstName: "SendAsHost"})
	if err != nil {
		t.Fatalf("create host: %v", err)
	}

	channels := NewChannelStore(pool)
	channelIDs := make([]int64, 0, 5)
	t.Cleanup(func() {
		for _, id := range channelIDs {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", id)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, host.ID})
	})
	mk := func(req domain.CreateChannelRequest) domain.CreateChannelResult {
		created, err := channels.CreateChannel(ctx, req)
		if err != nil {
			t.Fatalf("create channel %q: %v", req.Title, err)
		}
		channelIDs = append(channelIDs, created.Channel.ID)
		return created
	}

	owned := mk(domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "SA Owned " + suffix, Broadcast: true, Date: 1700000700})
	postAdmin := mk(domain.CreateChannelRequest{CreatorUserID: host.ID, Title: "SA PostAdmin " + suffix, Broadcast: true, MemberUserIDs: []int64{owner.ID}, Date: 1700000701})
	editAdmin := mk(domain.CreateChannelRequest{CreatorUserID: host.ID, Title: "SA EditAdmin " + suffix, Broadcast: true, MemberUserIDs: []int64{owner.ID}, Date: 1700000702})
	megagroup := mk(domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "SA Megagroup " + suffix, Megagroup: true, Date: 1700000703})
	memberOnly := mk(domain.CreateChannelRequest{CreatorUserID: host.ID, Title: "SA Member " + suffix, Broadcast: true, MemberUserIDs: []int64{owner.ID}, Date: 1700000704})

	if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		UserID:      host.ID,
		ChannelID:   postAdmin.Channel.ID,
		MemberID:    owner.ID,
		AdminRights: domain.ChannelAdminRights{PostMessages: true},
		Date:        1700000711,
	}); err != nil {
		t.Fatalf("edit post-messages admin: %v", err)
	}
	if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		UserID:      host.ID,
		ChannelID:   editAdmin.Channel.ID,
		MemberID:    owner.ID,
		AdminRights: domain.ChannelAdminRights{EditMessages: true, DeleteMessages: true},
		Date:        1700000712,
	}); err != nil {
		t.Fatalf("edit edit-only admin: %v", err)
	}

	list, err := channels.ListSendAsChannels(ctx, owner.ID)
	if err != nil {
		t.Fatalf("ListSendAsChannels: %v", err)
	}
	got := make(map[int64]bool, len(list))
	for _, ch := range list {
		got[ch.ID] = true
	}
	if !got[owned.Channel.ID] || !got[postAdmin.Channel.ID] {
		t.Fatalf("send-as channels %v missing creator-owned %d or post-admin %d", got, owned.Channel.ID, postAdmin.Channel.ID)
	}
	if got[editAdmin.Channel.ID] || got[megagroup.Channel.ID] || got[memberOnly.Channel.ID] {
		t.Fatalf("send-as channels %v should exclude edit-admin %d, megagroup %d, member-only %d", got, editAdmin.Channel.ID, megagroup.Channel.ID, memberOnly.Channel.ID)
	}
}

func TestChannelStoreCommonChannelsOnlySharedMegagroups(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)
	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{AccessHash: 51, Phone: "+1888" + suffix + "01", FirstName: "CommonOwner"})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	friend, err := users.Create(ctx, domain.User{AccessHash: 52, Phone: "+1888" + suffix + "02", FirstName: "CommonFriend"})
	if err != nil {
		t.Fatalf("create friend: %v", err)
	}
	other, err := users.Create(ctx, domain.User{AccessHash: 53, Phone: "+1888" + suffix + "03", FirstName: "CommonOther"})
	if err != nil {
		t.Fatalf("create other: %v", err)
	}
	var channelIDs []int64
	t.Cleanup(func() {
		if len(channelIDs) != 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID, other.ID})
	})

	channels := NewChannelStore(pool)
	create := func(title string, broadcast bool, memberIDs []int64, date int) domain.CreateChannelResult {
		t.Helper()
		created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
			CreatorUserID: owner.ID,
			Title:         title,
			Broadcast:     broadcast,
			Megagroup:     !broadcast,
			MemberUserIDs: memberIDs,
			Date:          date,
		})
		if err != nil {
			t.Fatalf("create %s: %v", title, err)
		}
		channelIDs = append(channelIDs, created.Channel.ID)
		return created
	}
	first := create("common one "+suffix, false, []int64{friend.ID}, 1700000800)
	second := create("common two "+suffix, false, []int64{friend.ID}, 1700000801)
	create("broadcast excluded "+suffix, true, []int64{friend.ID}, 1700000802)
	left := create("left excluded "+suffix, false, []int64{friend.ID}, 1700000803)
	if _, err := channels.LeaveChannel(ctx, left.Channel.ID, friend.ID, 1700000804); err != nil {
		t.Fatalf("leave channel: %v", err)
	}
	create("not shared "+suffix, false, []int64{other.ID}, 1700000805)

	page, err := channels.ListCommonChannels(ctx, domain.CommonChannelsRequest{
		UserID:       owner.ID,
		TargetUserID: friend.ID,
		Limit:        10,
	})
	if err != nil {
		t.Fatalf("list common channels: %v", err)
	}
	if page.Count != 2 || len(page.Channels) != 2 || page.Channels[0].ID != first.Channel.ID || page.Channels[1].ID != second.Channel.ID {
		t.Fatalf("common channels = %+v, want two shared megagroups in id order", page)
	}

	next, err := channels.ListCommonChannels(ctx, domain.CommonChannelsRequest{
		UserID:       owner.ID,
		TargetUserID: friend.ID,
		MaxID:        first.Channel.ID,
		Limit:        1,
	})
	if err != nil {
		t.Fatalf("list common channels after max id: %v", err)
	}
	if next.Count != 2 || len(next.Channels) != 1 || next.Channels[0].ID != second.Channel.ID {
		t.Fatalf("paged common channels = %+v, want second channel with full count", next)
	}

	countOnly, err := channels.ListCommonChannels(ctx, domain.CommonChannelsRequest{
		UserID:       owner.ID,
		TargetUserID: friend.ID,
		CountOnly:    true,
	})
	if err != nil {
		t.Fatalf("count common channels: %v", err)
	}
	if countOnly.Count != 2 || len(countOnly.Channels) != 0 {
		t.Fatalf("count-only common channels = %+v, want count without channels", countOnly)
	}
}

func TestChannelStoreListActiveChannelIDsForUserUsesMembershipIndexState(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)
	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{AccessHash: 81, Phone: "+1888" + suffix + "41", FirstName: "ActiveIndexOwner"})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	friend, err := users.Create(ctx, domain.User{AccessHash: 82, Phone: "+1888" + suffix + "42", FirstName: "ActiveIndexFriend"})
	if err != nil {
		t.Fatalf("create friend: %v", err)
	}
	var channelIDs []int64
	t.Cleanup(func() {
		if len(channelIDs) != 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
	})

	channels := NewChannelStore(pool)
	create := func(title string, date int) domain.CreateChannelResult {
		t.Helper()
		created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
			CreatorUserID: owner.ID,
			Title:         title,
			Megagroup:     true,
			MemberUserIDs: []int64{friend.ID},
			Date:          date,
		})
		if err != nil {
			t.Fatalf("create %s: %v", title, err)
		}
		channelIDs = append(channelIDs, created.Channel.ID)
		return created
	}
	activeA := create("active index one "+suffix, 1700000830)
	left := create("left index excluded "+suffix, 1700000831)
	activeB := create("active index two "+suffix, 1700000832)
	deleted := create("deleted index excluded "+suffix, 1700000833)
	if _, err := channels.LeaveChannel(ctx, left.Channel.ID, friend.ID, 1700000834); err != nil {
		t.Fatalf("leave channel: %v", err)
	}
	if _, err := channels.DeleteChannel(ctx, domain.DeleteChannelRequest{
		UserID:    owner.ID,
		ChannelID: deleted.Channel.ID,
		Date:      1700000835,
	}); err != nil {
		t.Fatalf("delete channel: %v", err)
	}

	want := []int64{activeA.Channel.ID, activeB.Channel.ID}
	if want[0] > want[1] {
		want[0], want[1] = want[1], want[0]
	}
	got, err := channels.ListActiveChannelIDsForUser(ctx, friend.ID, 0, 10)
	if err != nil {
		t.Fatalf("list active channel ids: %v", err)
	}
	if len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
		t.Fatalf("active channel ids = %v, want %v", got, want)
	}

	firstPage, err := channels.ListActiveChannelIDsForUser(ctx, friend.ID, 0, 1)
	if err != nil {
		t.Fatalf("list first active channel id: %v", err)
	}
	if len(firstPage) != 1 || firstPage[0] != want[0] {
		t.Fatalf("first active page = %v, want [%d]", firstPage, want[0])
	}
	nextPage, err := channels.ListActiveChannelIDsForUser(ctx, friend.ID, want[0], 10)
	if err != nil {
		t.Fatalf("list next active channel ids: %v", err)
	}
	if len(nextPage) != 1 || nextPage[0] != want[1] {
		t.Fatalf("next active page = %v, want [%d]", nextPage, want[1])
	}
	empty, err := channels.ListActiveChannelIDsForUser(ctx, friend.ID, want[1], 10)
	if err != nil {
		t.Fatalf("list active channel ids after last: %v", err)
	}
	if len(empty) != 0 {
		t.Fatalf("active ids after last = %v, want empty", empty)
	}
	if _, err := channels.ListActiveChannelIDsForUser(ctx, friend.ID, -1, 10); !errors.Is(err, domain.ErrChannelInvalid) {
		t.Fatalf("negative cursor err = %v, want ErrChannelInvalid", err)
	}
}

func TestChannelStoreUserChannelMemberIndexListPaths(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)
	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{AccessHash: 91, Phone: "+1888" + suffix + "51", FirstName: "IndexListOwner"})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	friend, err := users.Create(ctx, domain.User{AccessHash: 92, Phone: "+1888" + suffix + "52", FirstName: "IndexListFriend"})
	if err != nil {
		t.Fatalf("create friend: %v", err)
	}
	var channelIDs []int64
	t.Cleanup(func() {
		if len(channelIDs) != 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
	})

	channels := NewChannelStore(pool)
	publicCandidate, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "index public " + suffix,
		Megagroup:     true,
		Date:          1700000840,
	})
	if err != nil {
		t.Fatalf("create public candidate: %v", err)
	}
	channelIDs = append(channelIDs, publicCandidate.Channel.ID)
	if _, err := channels.UpdateUsername(ctx, domain.UpdateChannelUsernameRequest{
		UserID:    owner.ID,
		ChannelID: publicCandidate.Channel.ID,
		Username:  "idxpublic" + suffix,
	}); err != nil {
		t.Fatalf("set public username: %v", err)
	}
	admined, err := channels.ListAdminedPublicChannels(ctx, owner.ID)
	if err != nil {
		t.Fatalf("list admined public channels: %v", err)
	}
	if len(admined) != 1 || admined[0].ID != publicCandidate.Channel.ID {
		t.Fatalf("admined public = %+v, want public candidate", admined)
	}
	if _, err := channels.UpdateUsername(ctx, domain.UpdateChannelUsernameRequest{
		UserID:    owner.ID,
		ChannelID: publicCandidate.Channel.ID,
		Username:  "",
	}); err != nil {
		t.Fatalf("clear public username: %v", err)
	}
	admined, err = channels.ListAdminedPublicChannels(ctx, owner.ID)
	if err != nil {
		t.Fatalf("list admined public channels after clear: %v", err)
	}
	if len(admined) != 0 {
		t.Fatalf("admined public after clear = %+v, want empty", admined)
	}

	discussionCandidate, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "index discussion " + suffix,
		Megagroup:     true,
		MemberUserIDs: []int64{friend.ID},
		Date:          1700000841,
	})
	if err != nil {
		t.Fatalf("create discussion candidate: %v", err)
	}
	channelIDs = append(channelIDs, discussionCandidate.Channel.ID)
	friendGroups, err := channels.ListDiscussionGroups(ctx, friend.ID, 10)
	if err != nil {
		t.Fatalf("list friend discussion groups before admin: %v", err)
	}
	if len(friendGroups) != 0 {
		t.Fatalf("friend discussion groups before admin = %+v, want empty", friendGroups)
	}
	if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		UserID:    owner.ID,
		ChannelID: discussionCandidate.Channel.ID,
		MemberID:  friend.ID,
		AdminRights: domain.ChannelAdminRights{
			PinMessages: true,
		},
		Date: 1700000842,
	}); err != nil {
		t.Fatalf("grant pin admin: %v", err)
	}
	friendGroups, err = channels.ListDiscussionGroups(ctx, friend.ID, 10)
	if err != nil {
		t.Fatalf("list friend discussion groups after admin: %v", err)
	}
	if len(friendGroups) != 1 || friendGroups[0].ID != discussionCandidate.Channel.ID {
		t.Fatalf("friend discussion groups after admin = %+v, want candidate", friendGroups)
	}
	if _, err := channels.SetForum(ctx, owner.ID, discussionCandidate.Channel.ID, true, false); err != nil {
		t.Fatalf("enable forum: %v", err)
	}
	friendGroups, err = channels.ListDiscussionGroups(ctx, friend.ID, 10)
	if err != nil {
		t.Fatalf("list friend discussion groups after forum: %v", err)
	}
	if len(friendGroups) != 0 {
		t.Fatalf("friend discussion groups after forum = %+v, want empty", friendGroups)
	}
}

func TestChannelStoreListStoryPostableChannelsPostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)

	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{AccessHash: 9901, Phone: "+1888" + suffix + "41", FirstName: "StoryPostOwner"})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	admin, err := users.Create(ctx, domain.User{AccessHash: 9902, Phone: "+1888" + suffix + "42", FirstName: "StoryPostAdmin"})
	if err != nil {
		t.Fatalf("create admin: %v", err)
	}
	var channelIDs []int64
	t.Cleanup(func() {
		if len(channelIDs) > 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, admin.ID})
	})

	channels := NewChannelStore(pool)
	owned, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: admin.ID,
		Title:         "pg owned story " + suffix,
		Broadcast:     true,
		Date:          1700003400,
	})
	if err != nil {
		t.Fatalf("create owned channel: %v", err)
	}
	channelIDs = append(channelIDs, owned.Channel.ID)
	postable, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "pg postable story " + suffix,
		Broadcast:     true,
		MemberUserIDs: []int64{admin.ID},
		Date:          1700003401,
	})
	if err != nil {
		t.Fatalf("create postable channel: %v", err)
	}
	channelIDs = append(channelIDs, postable.Channel.ID)
	if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		ChannelID: postable.Channel.ID,
		UserID:    owner.ID,
		MemberID:  admin.ID,
		AdminRights: domain.ChannelAdminRights{
			PostStories: true,
		},
		Date: 1700003402,
	}); err != nil {
		t.Fatalf("grant post stories: %v", err)
	}
	editOnly, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "pg edit-only story " + suffix,
		Broadcast:     true,
		MemberUserIDs: []int64{admin.ID},
		Date:          1700003403,
	})
	if err != nil {
		t.Fatalf("create edit-only channel: %v", err)
	}
	channelIDs = append(channelIDs, editOnly.Channel.ID)
	if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
		ChannelID: editOnly.Channel.ID,
		UserID:    owner.ID,
		MemberID:  admin.ID,
		AdminRights: domain.ChannelAdminRights{
			EditStories: true,
		},
		Date: 1700003404,
	}); err != nil {
		t.Fatalf("grant edit stories only: %v", err)
	}

	list, err := channels.ListStoryPostableChannels(ctx, admin.ID)
	if err != nil {
		t.Fatalf("ListStoryPostableChannels: %v", err)
	}
	got := make([]int64, 0, len(list))
	for _, channel := range list {
		got = append(got, channel.ID)
	}
	want := []int64{postable.Channel.ID, owned.Channel.ID}
	if len(got) != len(want) {
		t.Fatalf("story postable channel ids = %v, want %v; excluded edit-only=%d", got, want, editOnly.Channel.ID)
	}
	if got[0] != want[0] || got[1] != want[1] {
		t.Fatalf("story postable channel ids = %v, want %v; excluded edit-only=%d", got, want, editOnly.Channel.ID)
	}
}

func TestChannelStoreLeftChannelsReturnsPagedLeftMemberships(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)
	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{AccessHash: 61, Phone: "+1889" + suffix + "01", FirstName: "LeftOwner"})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	friend, err := users.Create(ctx, domain.User{AccessHash: 62, Phone: "+1889" + suffix + "02", FirstName: "LeftFriend"})
	if err != nil {
		t.Fatalf("create friend: %v", err)
	}
	var channelIDs []int64
	t.Cleanup(func() {
		if len(channelIDs) != 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
	})

	channels := NewChannelStore(pool)
	create := func(title string, broadcast bool, date int) domain.CreateChannelResult {
		t.Helper()
		created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
			CreatorUserID: owner.ID,
			Title:         title,
			Broadcast:     broadcast,
			Megagroup:     !broadcast,
			MemberUserIDs: []int64{friend.ID},
			Date:          date,
		})
		if err != nil {
			t.Fatalf("create %s: %v", title, err)
		}
		channelIDs = append(channelIDs, created.Channel.ID)
		return created
	}
	older := create("older left "+suffix, false, 1700000810)
	newer := create("newer left "+suffix, true, 1700000811)
	create("active excluded "+suffix, false, 1700000812)
	if _, err := channels.LeaveChannel(ctx, older.Channel.ID, friend.ID, 1700000813); err != nil {
		t.Fatalf("leave older channel: %v", err)
	}
	if _, err := channels.LeaveChannel(ctx, newer.Channel.ID, friend.ID, 1700000814); err != nil {
		t.Fatalf("leave newer channel: %v", err)
	}

	page, err := channels.ListLeftChannels(ctx, friend.ID, 0, 1)
	if err != nil {
		t.Fatalf("list left channels: %v", err)
	}
	if page.Count != 2 || len(page.Channels) != 1 || page.Channels[0].Channel.ID != newer.Channel.ID || page.Channels[0].Self.Status != domain.ChannelMemberLeft {
		t.Fatalf("first left page = %+v, want newest left channel and full count", page)
	}
	next, err := channels.ListLeftChannels(ctx, friend.ID, 1, 1)
	if err != nil {
		t.Fatalf("list next left channels: %v", err)
	}
	if next.Count != 2 || len(next.Channels) != 1 || next.Channels[0].Channel.ID != older.Channel.ID {
		t.Fatalf("second left page = %+v, want older left channel", next)
	}
	empty, err := channels.ListLeftChannels(ctx, friend.ID, 2, 1)
	if err != nil {
		t.Fatalf("list empty left page: %v", err)
	}
	if empty.Count != 2 || len(empty.Channels) != 0 {
		t.Fatalf("empty left page = %+v, want full count and no chats", empty)
	}
	if _, err := channels.ListLeftChannels(ctx, friend.ID, domain.MaxLeftChannelsOffset+1, 1); !errors.Is(err, domain.ErrChannelInvalid) {
		t.Fatalf("huge offset err = %v, want ErrChannelInvalid", err)
	}
}

func TestChannelStoreDiscussionGroupLinksAreBidirectional(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)
	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{AccessHash: 71, Phone: "+1890" + suffix + "01", FirstName: "DiscussionOwner"})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	var channelIDs []int64
	t.Cleanup(func() {
		if len(channelIDs) != 0 {
			_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
		}
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
	})

	channels := NewChannelStore(pool)
	create := func(title string, broadcast bool, date int) domain.CreateChannelResult {
		t.Helper()
		created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
			CreatorUserID: owner.ID,
			Title:         title,
			Broadcast:     broadcast,
			Megagroup:     !broadcast,
			Date:          date,
		})
		if err != nil {
			t.Fatalf("create %s: %v", title, err)
		}
		channelIDs = append(channelIDs, created.Channel.ID)
		return created
	}
	broadcast := create("discussion broadcast "+suffix, true, 1700000820)
	firstGroup := create("discussion first "+suffix, false, 1700000821)
	secondGroup := create("discussion second "+suffix, false, 1700000822)

	candidates, err := channels.ListDiscussionGroups(ctx, owner.ID, 10)
	if err != nil {
		t.Fatalf("list discussion groups: %v", err)
	}
	if len(candidates) < 2 || candidates[0].ID != secondGroup.Channel.ID || candidates[1].ID != firstGroup.Channel.ID {
		t.Fatalf("discussion groups = %+v, want newest creator megagroups", candidates)
	}

	linked, err := channels.SetDiscussionGroup(ctx, owner.ID, broadcast.Channel.ID, firstGroup.Channel.ID)
	if err != nil {
		t.Fatalf("link first discussion group: %v", err)
	}
	if len(linked.Channels) != 2 {
		t.Fatalf("linked changed channels = %+v, want broadcast and group", linked.Channels)
	}
	gotBroadcast, err := channels.GetChannelByID(ctx, broadcast.Channel.ID)
	if err != nil {
		t.Fatalf("get linked broadcast: %v", err)
	}
	gotFirst, err := channels.GetChannelByID(ctx, firstGroup.Channel.ID)
	if err != nil {
		t.Fatalf("get linked first group: %v", err)
	}
	if gotBroadcast.LinkedChatID != firstGroup.Channel.ID || gotFirst.LinkedChatID != broadcast.Channel.ID {
		t.Fatalf("first link = broadcast %d group %d, want bidirectional", gotBroadcast.LinkedChatID, gotFirst.LinkedChatID)
	}

	replaced, err := channels.SetDiscussionGroup(ctx, owner.ID, broadcast.Channel.ID, secondGroup.Channel.ID)
	if err != nil {
		t.Fatalf("replace discussion group: %v", err)
	}
	if len(replaced.Channels) != 3 {
		t.Fatalf("replace changed channels = %+v, want broadcast old group new group", replaced.Channels)
	}
	gotBroadcast, _ = channels.GetChannelByID(ctx, broadcast.Channel.ID)
	gotFirst, _ = channels.GetChannelByID(ctx, firstGroup.Channel.ID)
	gotSecond, err := channels.GetChannelByID(ctx, secondGroup.Channel.ID)
	if err != nil {
		t.Fatalf("get linked second group: %v", err)
	}
	if gotBroadcast.LinkedChatID != secondGroup.Channel.ID || gotSecond.LinkedChatID != broadcast.Channel.ID || gotFirst.LinkedChatID != 0 {
		t.Fatalf("replace link = broadcast %d first %d second %d, want old cleared and new bidirectional",
			gotBroadcast.LinkedChatID, gotFirst.LinkedChatID, gotSecond.LinkedChatID)
	}

	if _, err := channels.SetDiscussionGroup(ctx, owner.ID, 0, secondGroup.Channel.ID); err != nil {
		t.Fatalf("unlink from group side: %v", err)
	}
	gotBroadcast, _ = channels.GetChannelByID(ctx, broadcast.Channel.ID)
	gotSecond, _ = channels.GetChannelByID(ctx, secondGroup.Channel.ID)
	if gotBroadcast.LinkedChatID != 0 || gotSecond.LinkedChatID != 0 {
		t.Fatalf("unlink = broadcast %d group %d, want both cleared", gotBroadcast.LinkedChatID, gotSecond.LinkedChatID)
	}
	if _, err := channels.SetDiscussionGroup(ctx, owner.ID, 0, secondGroup.Channel.ID); !errors.Is(err, domain.ErrLinkNotModified) {
		t.Fatalf("repeat unlink err = %v, want ErrLinkNotModified", err)
	}
	if _, err := channels.SetPreHistoryHidden(ctx, owner.ID, firstGroup.Channel.ID, true); err != nil {
		t.Fatalf("hide first group prehistory: %v", err)
	}
	if _, err := channels.SetDiscussionGroup(ctx, owner.ID, broadcast.Channel.ID, firstGroup.Channel.ID); !errors.Is(err, domain.ErrMegagroupPrehistoryHidden) {
		t.Fatalf("hidden prehistory err = %v, want ErrMegagroupPrehistoryHidden", err)
	}
}