Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/activitypub/fediverse-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
buildUpdateActivityAndObjectFromPost,
} from '@/helpers/activitypub/activity';
import { PostType } from '@/post/post.entity';
import type { KnexPostRepository } from '@/post/post.repository.knex';
import { PostCreatedEvent } from '@/post/post-created.event';
import { PostDeletedEvent } from '@/post/post-deleted.event';
import { PostUpdatedEvent } from '@/post/post-updated.event';
Expand All @@ -28,6 +29,7 @@ export class FediverseBridge {
private readonly events: EventEmitter,
private readonly fedifyContextFactory: FedifyContextFactory,
private readonly accountService: AccountService,
private readonly postRepository: KnexPostRepository,
) {}

async init() {
Expand Down Expand Up @@ -138,8 +140,8 @@ export class FediverseBridge {
}

private async handlePostUpdated(event: PostUpdatedEvent) {
const post = event.getPost();
if (!post.author.isInternal) {
const post = await this.postRepository.getById(event.getPostId());
if (!post || !post.author.isInternal) {
return;
}

Expand Down
36 changes: 34 additions & 2 deletions src/activitypub/fediverse-bridge.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FediverseBridge } from '@/activitypub/fediverse-bridge';
import type { UriBuilder } from '@/activitypub/uri';
import type { FedifyContext } from '@/app';
import { Post, PostType } from '@/post/post.entity';
import type { KnexPostRepository } from '@/post/post.repository.knex';
import { PostCreatedEvent } from '@/post/post-created.event';
import { PostDeletedEvent } from '@/post/post-deleted.event';
import { PostUpdatedEvent } from '@/post/post-updated.event';
Expand All @@ -29,6 +30,7 @@ vi.mock('node:crypto', async (importOriginal) => {
describe('FediverseBridge', () => {
let events: EventEmitter;
let accountService: AccountService;
let postRepository: KnexPostRepository;
let context: FedifyContext;
let fedifyContextFactory: FedifyContextFactory;
let mockUriBuilder: UriBuilder<FedifyObject>;
Expand All @@ -39,6 +41,9 @@ describe('FediverseBridge', () => {
accountService = {
getAccountById: vi.fn(),
} as unknown as AccountService;
postRepository = {
getById: vi.fn(),
} as unknown as KnexPostRepository;
mockUriBuilder = {
buildObjectUri: vi.fn().mockImplementation((object, { id }) => {
return new URL(
Expand Down Expand Up @@ -75,6 +80,7 @@ describe('FediverseBridge', () => {
events,
fedifyContextFactory,
accountService,
postRepository,
);
});

Expand Down Expand Up @@ -163,6 +169,7 @@ describe('FediverseBridge', () => {
events,
fedifyContextFactory,
accountService,
postRepository,
);
await bridge.init();

Expand Down Expand Up @@ -246,6 +253,7 @@ describe('FediverseBridge', () => {
events,
fedifyContextFactory,
accountService,
postRepository,
);
await bridge.init();

Expand Down Expand Up @@ -291,6 +299,7 @@ describe('FediverseBridge', () => {
events,
fedifyContextFactory,
accountService,
postRepository,
);
await bridge.init();

Expand Down Expand Up @@ -333,6 +342,7 @@ describe('FediverseBridge', () => {
events,
fedifyContextFactory,
accountService,
postRepository,
);
await bridge.init();

Expand Down Expand Up @@ -515,10 +525,13 @@ describe('FediverseBridge', () => {
post.apId = new URL('https://example.com/article/post-456');
post.publishedAt = new Date('2025-01-01T00:00:00Z');

const event = new PostUpdatedEvent(post);
vi.mocked(postRepository.getById).mockResolvedValue(post);

const event = new PostUpdatedEvent(456);
events.emit(PostUpdatedEvent.getName(), event);

await nextTick();
expect(postRepository.getById).toHaveBeenCalledWith(456);
expect(sendActivity).toHaveBeenCalledOnce();
expect(context.data.globaldb.set).toHaveBeenCalledTimes(2);

Expand Down Expand Up @@ -547,10 +560,29 @@ describe('FediverseBridge', () => {
post.content = 'Updated post content';
post.apId = new URL('https://external.com/article/post-456');

const event = new PostUpdatedEvent(post);
vi.mocked(postRepository.getById).mockResolvedValue(post);

const event = new PostUpdatedEvent(456);
events.emit(PostUpdatedEvent.getName(), event);

await nextTick();
expect(postRepository.getById).toHaveBeenCalledWith(456);
expect(sendActivity).not.toHaveBeenCalled();
expect(context.data.globaldb.set).not.toHaveBeenCalled();
});

it('should not send update activities on the PostUpdatedEvent if post is not found', async () => {
await bridge.init();

const sendActivity = vi.spyOn(context, 'sendActivity');

vi.mocked(postRepository.getById).mockResolvedValue(null);

const event = new PostUpdatedEvent(999);
events.emit(PostUpdatedEvent.getName(), event);

await nextTick();
expect(postRepository.getById).toHaveBeenCalledWith(999);
expect(sendActivity).not.toHaveBeenCalled();
expect(context.data.globaldb.set).not.toHaveBeenCalled();
});
Expand Down
4 changes: 4 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ import type { PostInteractionCountsService } from '@/post/post-interaction-count
import { PostInteractionCountsUpdateRequestedEvent } from '@/post/post-interaction-counts-update-requested.event';
import { PostLikedEvent } from '@/post/post-liked.event';
import { PostRepostedEvent } from '@/post/post-reposted.event';
import { PostUpdatedEvent } from '@/post/post-updated.event';
import type { Site } from '@/site/site.service';

function toLogLevel(level: unknown): LogLevel | null {
Expand Down Expand Up @@ -279,6 +280,9 @@ container
container
.resolve<EventSerializer>('eventSerializer')
.register(PostRepostedEvent.getName(), PostRepostedEvent);
container
.resolve<EventSerializer>('eventSerializer')
.register(PostUpdatedEvent.getName(), PostUpdatedEvent);

/** Fedify */

Expand Down
27 changes: 22 additions & 5 deletions src/post/post-updated.event.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import type { Post } from '@/post/post.entity';
import type { SerializableEvent } from '@/events/event';

export class PostUpdatedEvent {
constructor(private readonly post: Post) {}
export class PostUpdatedEvent implements SerializableEvent {
constructor(private readonly postId: number) {}

getPost(): Post {
return this.post;
getPostId(): number {
return this.postId;
}

getName(): string {
return PostUpdatedEvent.getName();
}

static getName(): string {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent lets add the non-static version like in the other events whilst we are here

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look!

Open in Cursor Open in Web

return 'post.updated';
}

toJSON(): Record<string, unknown> {
return {
postId: this.postId,
};
}

static fromJSON(data: Record<string, unknown>): PostUpdatedEvent {
if (typeof data.postId !== 'number') {
throw new Error('postId must be a number');
}
return new PostUpdatedEvent(data.postId);
}
}
77 changes: 77 additions & 0 deletions src/post/post-updated.event.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it } from 'vitest';

import { PostUpdatedEvent } from '@/post/post-updated.event';

describe('PostUpdatedEvent', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent These tests should be consistent with the other events

Copy link

@cursor cursor bot Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restructured the PostUpdatedEvent tests to be consistent with other event tests:

  • Added nested describe blocks for each method (getName, getPostId, toJSON, fromJSON, round-trip serialization)
  • Added test case for postId being null
  • Added round-trip serialization test
  • Using .toBe() instead of .toEqual() for primitive comparisons

Open in Cursor Open in Web

describe('getName', () => {
it('should return the event name from static method', () => {
expect(PostUpdatedEvent.getName()).toBe('post.updated');
});

it('should return the event name from instance method', () => {
const event = new PostUpdatedEvent(123);

expect(event.getName()).toBe('post.updated');
});
});

describe('getPostId', () => {
it('should return the post id', () => {
const event = new PostUpdatedEvent(123);

expect(event.getPostId()).toBe(123);
});
});

describe('toJSON', () => {
it('should serialize the event to JSON', () => {
const event = new PostUpdatedEvent(123);

expect(event.toJSON()).toEqual({
postId: 123,
});
});
});

describe('fromJSON', () => {
it('should deserialize the event from JSON', () => {
const event = PostUpdatedEvent.fromJSON({
postId: 123,
});

expect(event.getPostId()).toBe(123);
});

it('should throw an error if postId is missing', () => {
expect(() => PostUpdatedEvent.fromJSON({})).toThrow(
'postId must be a number',
);
});

it('should throw an error if postId is not a number', () => {
expect(() =>
PostUpdatedEvent.fromJSON({
postId: 'not a number',
}),
).toThrow('postId must be a number');
});

it('should throw an error if postId is null', () => {
expect(() =>
PostUpdatedEvent.fromJSON({
postId: null,
}),
).toThrow('postId must be a number');
});
});

describe('round-trip serialization', () => {
it('should correctly serialize and deserialize', () => {
const original = new PostUpdatedEvent(999);
const json = original.toJSON();
const restored = PostUpdatedEvent.fromJSON(json);

expect(restored.getPostId()).toBe(original.getPostId());
});
});
});
6 changes: 3 additions & 3 deletions src/post/post.repository.knex.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2094,14 +2094,14 @@ describe('KnexPostRepository', () => {
expect(eventsEmitSpy).toHaveBeenCalledWith(
PostUpdatedEvent.getName(),
expect.objectContaining({
getPost: expect.any(Function),
getPostId: expect.any(Function),
}),
);

const emittedEvent = eventsEmitSpy.mock.calls.find(
(call) => call[0] === PostUpdatedEvent.getName(),
)?.[1];
expect(emittedEvent?.getPost()).toEqual(post);
)?.[1] as PostUpdatedEvent | undefined;
expect(emittedEvent?.getPostId()).toEqual(post.id);

const updatedRowInDb = await client('posts')
.where({ uuid: post.uuid })
Expand Down
2 changes: 1 addition & 1 deletion src/post/post.repository.knex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export class KnexPostRepository {
if (wasUpdated) {
await this.events.emitAsync(
PostUpdatedEvent.getName(),
new PostUpdatedEvent(post),
new PostUpdatedEvent(post.id as number),
);
}

Expand Down