Ctrl K
ComunidadGuías…

internal/store/postgres/channel_message_fact_check_integration_test.go

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

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"errors"
	"testing"

	appchannels "telesrv/internal/app/channels"
	"telesrv/internal/domain"
)

// TestChannelFactCheckSurvivesReadPaths verifies the Layer 231 fact-check label
// lives in the channel_messages.factcheck column, and that:
//   - the author can write the verdict, an ordinary non-author cannot;
//   - the read path (GetChannelMessages) carries the label back;
//   - batch reading (GetFactChecks) returns the same verdict;
//   - the label disappears after deletion.
func TestChannelFactCheckSurvivesReadPaths(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: "+1997" + suffix + "01", FirstName: "FactOwner"})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	member, err := users.Create(ctx, domain.User{AccessHash: 92, Phone: "+1997" + suffix + "02", FirstName: "FactMember"})
	if err != nil {
		t.Fatalf("create member: %v", err)
	}
	ids := []int64{owner.ID, member.ID}
	t.Cleanup(func() {
		_, _ = pool.Exec(ctx, "DELETE FROM channel_update_events WHERE channel_id = ANY($1::bigint[])", []int64{})
		_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", ids)
		_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", ids)
	})

	channels := NewChannelStore(pool)
	created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID, Title: "Fact Check " + suffix, Broadcast: true, Date: 1_700_100_000,
	})
	if err != nil {
		t.Fatalf("create channel: %v", err)
	}
	channelID := created.Channel.ID
	t.Cleanup(func() {
		_, _ = pool.Exec(ctx, "DELETE FROM channel_update_events WHERE channel_id = $1", channelID)
		_, _ = pool.Exec(ctx, "DELETE FROM channel_messages WHERE channel_id = $1", channelID)
		_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
	})

	post, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
		UserID: owner.ID, ChannelID: channelID, RandomID: 1, Message: "post", Date: 1_700_100_100,
	})
	if err != nil {
		t.Fatalf("send post: %v", err)
	}
	if _, err := channels.JoinChannel(ctx, channelID, member.ID, 1_700_100_200); err != nil {
		t.Fatalf("join member: %v", err)
	}

	// An ordinary non-author member cannot write a fact check.
	if _, err := channels.EditFactCheck(ctx, domain.EditFactCheckRequest{
		UserID: member.ID, ChannelID: channelID, MessageID: post.Message.ID, Text: "not allowed", Date: 1_700_100_300,
	}); !errors.Is(err, domain.ErrFactCheckForbidden) {
		t.Fatalf("member editFactCheck error = %v, want ErrFactCheckForbidden", err)
	}

	// The author writes the verdict.
	res, err := channels.EditFactCheck(ctx, domain.EditFactCheckRequest{
		UserID: owner.ID, ChannelID: channelID, MessageID: post.Message.ID,
		Text: "The claim is false.", Country: "DE", Date: 1_700_100_400,
	})
	if err != nil {
		t.Fatalf("editFactCheck: %v", err)
	}
	if res.FactCheck == nil || res.FactCheck.Text != "The claim is false." {
		t.Fatalf("editFactCheck result = %+v", res.FactCheck)
	}
	if res.FactCheck.Hash == 0 {
		t.Fatal("fact check hash is zero")
	}

	// The read path carries the label back.
	history, err := channels.GetChannelMessages(ctx, owner.ID, channelID, []int{post.Message.ID})
	if err != nil {
		t.Fatalf("get channel messages: %v", err)
	}
	if len(history.Messages) != 1 {
		t.Fatalf("got %d messages, want 1", len(history.Messages))
	}
	got := history.Messages[0].FactCheck
	if got == nil {
		t.Fatal("channel message has no fact check")
	}
	if got.Text != "The claim is false." || got.Country != "DE" {
		t.Fatalf("fact check = %+v", got)
	}
	if got.Hash != res.FactCheck.Hash {
		t.Fatalf("fact check hash = %d, want %d", got.Hash, res.FactCheck.Hash)
	}

	// Batch reading returns the same verdict.
	list, err := appchannels.NewService(channels).GetFactChecks(ctx, owner.ID, channelID, []int{post.Message.ID})
	if err != nil {
		t.Fatalf("getFactChecks: %v", err)
	}
	if len(list.FactChecks) != 1 || list.FactChecks[0].Text != "The claim is false." {
		t.Fatalf("getFactChecks = %+v", list.FactChecks)
	}
	service := appchannels.NewService(channels)
	ordered, err := service.GetFactChecks(ctx, member.ID, channelID, []int{999999, post.Message.ID, post.Message.ID})
	if err != nil || len(ordered.FactChecks) != 3 || ordered.FactChecks[0].Hash != 0 || ordered.FactChecks[1].Hash != got.Hash || ordered.FactChecks[2].Hash != got.Hash {
		t.Fatalf("ordered/duplicate batch = %+v, %v", ordered, err)
	}
	if _, err := channels.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{UserID: member.ID, ChannelID: channelID, MaxID: post.Message.ID, Date: 1_700_100_450}); err != nil {
		t.Fatal(err)
	}
	cleared, err := service.GetFactChecks(ctx, member.ID, channelID, []int{post.Message.ID})
	if err != nil || len(cleared.FactChecks) != 1 || cleared.FactChecks[0].Hash != 0 {
		t.Fatalf("cleared history verdict = %+v, %v", cleared, err)
	}
	ownerRead, err := service.GetFactChecks(ctx, owner.ID, channelID, []int{post.Message.ID})
	if err != nil || ownerRead.FactChecks[0].Hash == 0 {
		t.Fatalf("other viewer lost verdict: %+v, %v", ownerRead, err)
	}

	// The label disappears after deletion.
	if _, err := channels.EditFactCheck(ctx, domain.EditFactCheckRequest{
		UserID: owner.ID, ChannelID: channelID, MessageID: post.Message.ID, Text: "", Date: 1_700_100_500,
	}); err != nil {
		t.Fatalf("deleteFactCheck: %v", err)
	}
	history, err = channels.GetChannelMessages(ctx, owner.ID, channelID, []int{post.Message.ID})
	if err != nil {
		t.Fatalf("get channel messages after delete: %v", err)
	}
	if history.Messages[0].FactCheck != nil {
		t.Fatalf("message still carries a fact check: %+v", history.Messages[0].FactCheck)
	}
	// A fresh store reads both committed edits from the durable channel stream.
	difference, err := NewChannelStore(pool).ListChannelDifference(ctx, domain.ChannelDifferenceRequest{UserID: owner.ID, ChannelID: channelID, Pts: res.Event.Pts - 1, Limit: 20})
	if err != nil {
		t.Fatal(err)
	}
	var editChecks []*domain.FactCheck
	for _, event := range difference.Events {
		if event.Type == domain.ChannelUpdateEditMessage {
			editChecks = append(editChecks, event.Message.FactCheck)
		}
	}
	if len(editChecks) != 2 || editChecks[0] == nil || editChecks[1] != nil {
		t.Fatalf("durable edit/delete snapshots = %+v", editChecks)
	}
	if _, err := channels.EditFactCheck(ctx, domain.EditFactCheckRequest{UserID: owner.ID, ChannelID: channelID, MessageID: post.Message.ID, Text: "must disappear with the message"}); err != nil {
		t.Fatal(err)
	}
	if _, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{UserID: owner.ID, ChannelID: channelID, IDs: []int{post.Message.ID}, Date: 1_700_100_600}); err != nil {
		t.Fatal(err)
	}
	deleted, err := service.GetFactChecks(ctx, owner.ID, channelID, []int{post.Message.ID})
	if err != nil || len(deleted.FactChecks) != 1 || deleted.FactChecks[0].Hash != 0 {
		t.Fatalf("deleted message verdict = %+v, %v", deleted, err)
	}
}