A community based topic aggregation platform built on atproto

fix(votes): filter soft-deleted votes in GetByURI and fix E2E test event simulation

- Add `deleted_at IS NULL` filter to GetByURI() in vote repository
to properly exclude soft-deleted votes (matching GetByVoterAndSubject behavior)

- Fix TestVoteE2E_ToggleDifferentDirection to simulate correct event sequence:
When changing vote direction, the service DELETEs old vote and CREATEs new one
with a new rkey (not UPDATE). Test now simulates DELETE + CREATE events.

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+47 -16
internal
db
postgres
tests
integration
+3 -2
internal/db/postgres/vote_repo.go
···
return nil
}
-
// GetByURI retrieves a vote by its AT-URI
+
// GetByURI retrieves an active vote by its AT-URI
// Used by Jetstream consumer for DELETE operations
+
// Returns ErrVoteNotFound for soft-deleted votes
func (r *postgresVoteRepo) GetByURI(ctx context.Context, uri string) (*votes.Vote, error) {
query := `
SELECT
···
subject_uri, subject_cid, direction,
created_at, indexed_at, deleted_at
FROM votes
-
WHERE uri = $1
+
WHERE uri = $1 AND deleted_at IS NULL
`
var vote votes.Vote
+44 -14
tests/integration/vote_e2e_test.go
···
t.Logf("Failed to close response body: %v", closeErr)
}
-
// Simulate Jetstream UPDATE event (PDS updates the existing record)
-
t.Logf("\n๐Ÿ”„ Simulating Jetstream UPDATE event...")
-
updateEvent := jetstream.JetstreamEvent{
+
// The service flow for direction change is:
+
// 1. DELETE old vote on PDS
+
// 2. CREATE new vote with NEW rkey on PDS
+
// So we simulate DELETE + CREATE events (not UPDATE)
+
+
// Simulate Jetstream DELETE event for old vote
+
t.Logf("\n๐Ÿ”„ Simulating Jetstream DELETE event for old upvote...")
+
deleteEvent := jetstream.JetstreamEvent{
+
Did: userDID,
+
TimeUS: time.Now().UnixMicro(),
+
Kind: "commit",
+
Commit: &jetstream.CommitEvent{
+
Rev: "test-vote-rev-delete",
+
Operation: "delete",
+
Collection: "social.coves.feed.vote",
+
RKey: rkey, // Old upvote rkey
+
},
+
}
+
if handleErr := voteConsumer.HandleEvent(ctx, &deleteEvent); handleErr != nil {
+
t.Fatalf("Failed to handle delete event: %v", handleErr)
+
}
+
+
// Simulate Jetstream CREATE event for new downvote
+
t.Logf("\n๐Ÿ”„ Simulating Jetstream CREATE event for new downvote...")
+
newRkey := utils.ExtractRKeyFromURI(downvoteResp.URI)
+
createEvent := jetstream.JetstreamEvent{
Did: userDID,
TimeUS: time.Now().UnixMicro(),
Kind: "commit",
Commit: &jetstream.CommitEvent{
Rev: "test-vote-rev-down",
-
Operation: "update",
+
Operation: "create",
Collection: "social.coves.feed.vote",
-
RKey: rkey, // Same rkey as before
+
RKey: newRkey, // NEW rkey from downvote response
CID: downvoteResp.CID,
Record: map[string]interface{}{
"$type": "social.coves.feed.vote",
···
"uri": postURI,
"cid": postCID,
},
-
"direction": "down", // Changed direction
+
"direction": "down",
"createdAt": time.Now().Format(time.RFC3339),
},
},
}
-
if handleErr := voteConsumer.HandleEvent(ctx, &updateEvent); handleErr != nil {
-
t.Fatalf("Failed to handle update event: %v", handleErr)
+
if handleErr := voteConsumer.HandleEvent(ctx, &createEvent); handleErr != nil {
+
t.Fatalf("Failed to handle create event: %v", handleErr)
+
}
+
+
// Verify old upvote was deleted
+
t.Logf("\n๐Ÿ” Verifying old upvote was deleted...")
+
_, err = voteRepo.GetByURI(ctx, upvoteResp.URI)
+
if err == nil {
+
t.Error("Expected old upvote to be deleted, but it still exists")
}
-
// Verify vote direction changed in AppView
-
t.Logf("\n๐Ÿ” Verifying vote direction changed in AppView...")
-
updatedVote, err := voteRepo.GetByURI(ctx, upvoteResp.URI)
+
// Verify new downvote was indexed
+
t.Logf("\n๐Ÿ” Verifying new downvote indexed in AppView...")
+
newVote, err := voteRepo.GetByURI(ctx, downvoteResp.URI)
if err != nil {
-
t.Fatalf("Vote not found after update: %v", err)
+
t.Fatalf("New downvote not found: %v", err)
}
-
if updatedVote.Direction != "down" {
-
t.Errorf("Expected direction 'down', got %s", updatedVote.Direction)
+
if newVote.Direction != "down" {
+
t.Errorf("Expected direction 'down', got %s", newVote.Direction)
}
// Verify post counts updated