chore(test): cover processFriendRequestCreateEvent branches
Extends the existing happy-path test (one case) with 14 more covering every branch of the receiver-side handler: missing payload, attribution mismatch, tombstoned sender (silent accept), recipient_not_found (including soft-deleted recipient), already-friends idempotency in both column orderings, same-direction pending re-delivery idempotency, declined-then-resent (status filter), reused replicated stub, profile hydration with URL resolution, explicit and fallback createdAt, and mixed-batch accept/reject isolation through processRelayEvents.
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||||
import { eq } from 'drizzle-orm';
|
import { and, eq } from 'drizzle-orm';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import * as schema from '../db/schema.js';
|
import * as schema from '../db/schema.js';
|
||||||
import { setWorkerId } from '../utils/snowflake.js';
|
import { setWorkerId } from '../utils/snowflake.js';
|
||||||
import type { FederationRelayEvent } from '@backspace/shared';
|
import type { FederationRelayEvent, FederationFriendshipPayload } from '@backspace/shared';
|
||||||
|
|
||||||
setWorkerId(1);
|
setWorkerId(1);
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -43,6 +43,58 @@ beforeEach(() => {
|
|||||||
sendToUser.mockReset();
|
sendToUser.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function seedLocalUser(id: string, username: string): void {
|
||||||
|
testDb.insert(schema.users).values({
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
passwordHash: 'x',
|
||||||
|
status: 'online',
|
||||||
|
isAdmin: 0,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
} as typeof schema.users.$inferInsert).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedReplicatedUser(opts: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
homeUserId: string;
|
||||||
|
homeInstance: string;
|
||||||
|
isDeleted?: 0 | 1;
|
||||||
|
}): void {
|
||||||
|
testDb.insert(schema.users).values({
|
||||||
|
id: opts.id,
|
||||||
|
username: opts.username,
|
||||||
|
passwordHash: '!federation-replicated',
|
||||||
|
status: 'offline',
|
||||||
|
isAdmin: 0,
|
||||||
|
homeInstance: opts.homeInstance,
|
||||||
|
homeUserId: opts.homeUserId,
|
||||||
|
isDeleted: opts.isDeleted ?? 0,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
} as typeof schema.users.$inferInsert).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeEvent(opts: {
|
||||||
|
messageId?: string;
|
||||||
|
friendship?: Partial<FederationFriendshipPayload> & { from: FederationFriendshipPayload['from']; to: FederationFriendshipPayload['to'] };
|
||||||
|
omitFriendship?: boolean;
|
||||||
|
}): FederationRelayEvent {
|
||||||
|
const base: FederationRelayEvent = {
|
||||||
|
eventType: 'friend_request_create',
|
||||||
|
contextType: 'friend',
|
||||||
|
messageId: opts.messageId ?? 'msg-' + Math.random().toString(36).slice(2),
|
||||||
|
encryptionVersion: 0,
|
||||||
|
timestamp: 12345,
|
||||||
|
};
|
||||||
|
if (opts.omitFriendship) return base;
|
||||||
|
base.friendship = {
|
||||||
|
createdAt: 12345,
|
||||||
|
status: 'pending',
|
||||||
|
...opts.friendship,
|
||||||
|
} as FederationFriendshipPayload;
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
describe('processRelayEvents — friend_request_create from sender home (S2S friend-add wire format)', () => {
|
describe('processRelayEvents — friend_request_create from sender home (S2S friend-add wire format)', () => {
|
||||||
it('accepts the event, creates the local request, hydrates the sender stub, broadcasts to recipient', async () => {
|
it('accepts the event, creates the local request, hydrates the sender stub, broadcasts to recipient', async () => {
|
||||||
// Seed local recipient
|
// Seed local recipient
|
||||||
@@ -114,3 +166,402 @@ describe('processRelayEvents — friend_request_create from sender home (S2S fri
|
|||||||
expect(evt.request.user?.displayName).toBe('Bob');
|
expect(evt.request.user?.displayName).toBe('Bob');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('processFriendRequestCreateEvent — branch coverage', () => {
|
||||||
|
it('rejects event with missing friendship payload', async () => {
|
||||||
|
const event = makeEvent({ messageId: 'no-payload', omitFriendship: true });
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual([]);
|
||||||
|
expect(result.rejected).toEqual([{ messageId: 'no-payload', reason: 'missing_friendship_payload' }]);
|
||||||
|
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(0);
|
||||||
|
expect(sendToUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects event when sender attribution does not match peer origin', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
// Sender's homeInstance claims orbit.test, but peer signing the relay is impostor.test.
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'attrib-mismatch',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://impostor.test', 'https://impostor.test', testDb);
|
||||||
|
|
||||||
|
expect(result.rejected).toEqual([{ messageId: 'attrib-mismatch', reason: 'attribution_mismatch' }]);
|
||||||
|
expect(result.accepted).toEqual([]);
|
||||||
|
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(0);
|
||||||
|
expect(testDb.select().from(schema.users).where(eq(schema.users.homeUserId, 'remote-bob')).all()).toHaveLength(0);
|
||||||
|
expect(sendToUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('silently accepts when sender identity is tombstoned (deleted) on this instance', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
// Pre-existing tombstoned record for the sender — resolveOrCreateReplicatedUser refuses to resurrect.
|
||||||
|
seedReplicatedUser({
|
||||||
|
id: 'bob-tombstone',
|
||||||
|
username: 'remote-bob@orbit.test',
|
||||||
|
homeUserId: 'remote-bob',
|
||||||
|
homeInstance: 'orbit.test',
|
||||||
|
isDeleted: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'tombstoned-sender',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual(['tombstoned-sender']);
|
||||||
|
expect(result.rejected).toEqual([]);
|
||||||
|
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(0);
|
||||||
|
expect(sendToUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects with recipient_not_found when the recipient is not a local user', async () => {
|
||||||
|
// No alice seeded; the to.homeUserId points to nothing local.
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'no-recipient',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'unknown-recipient', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.rejected).toEqual([{ messageId: 'no-recipient', reason: 'recipient_not_found' }]);
|
||||||
|
expect(result.accepted).toEqual([]);
|
||||||
|
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(0);
|
||||||
|
expect(sendToUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects with recipient_not_found when the matched local user is soft-deleted', async () => {
|
||||||
|
// resolveLocalUser filters isDeleted=1, so a deleted recipient is treated as not found.
|
||||||
|
testDb.insert(schema.users).values({
|
||||||
|
id: 'alice-deleted',
|
||||||
|
username: 'alice',
|
||||||
|
passwordHash: 'x',
|
||||||
|
status: 'offline',
|
||||||
|
isAdmin: 0,
|
||||||
|
isDeleted: 1,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
} as typeof schema.users.$inferInsert).run();
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'recipient-deleted',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-deleted', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.rejected).toEqual([{ messageId: 'recipient-deleted', reason: 'recipient_not_found' }]);
|
||||||
|
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts idempotently when users are already friends (sender→recipient row)', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
seedReplicatedUser({
|
||||||
|
id: 'bob-stub',
|
||||||
|
username: 'remote-bob@orbit.test',
|
||||||
|
homeUserId: 'remote-bob',
|
||||||
|
homeInstance: 'orbit.test',
|
||||||
|
});
|
||||||
|
testDb.insert(schema.friends).values({
|
||||||
|
userId: 'bob-stub',
|
||||||
|
friendId: 'alice-id',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'already-friends-fwd',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual(['already-friends-fwd']);
|
||||||
|
expect(result.rejected).toEqual([]);
|
||||||
|
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(0);
|
||||||
|
expect(sendToUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts idempotently when users are already friends (recipient→sender row)', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
seedReplicatedUser({
|
||||||
|
id: 'bob-stub',
|
||||||
|
username: 'remote-bob@orbit.test',
|
||||||
|
homeUserId: 'remote-bob',
|
||||||
|
homeInstance: 'orbit.test',
|
||||||
|
});
|
||||||
|
// Friendship persisted in the reverse column order — handler must still detect it.
|
||||||
|
testDb.insert(schema.friends).values({
|
||||||
|
userId: 'alice-id',
|
||||||
|
friendId: 'bob-stub',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'already-friends-rev',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual(['already-friends-rev']);
|
||||||
|
expect(result.rejected).toEqual([]);
|
||||||
|
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(0);
|
||||||
|
expect(sendToUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts idempotently when a same-direction pending request already exists (re-delivery)', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
seedReplicatedUser({
|
||||||
|
id: 'bob-stub',
|
||||||
|
username: 'remote-bob@orbit.test',
|
||||||
|
homeUserId: 'remote-bob',
|
||||||
|
homeInstance: 'orbit.test',
|
||||||
|
});
|
||||||
|
testDb.insert(schema.friendRequests).values({
|
||||||
|
id: 'existing-req',
|
||||||
|
fromId: 'bob-stub',
|
||||||
|
toId: 'alice-id',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: 1000,
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'redelivery',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual(['redelivery']);
|
||||||
|
expect(result.rejected).toEqual([]);
|
||||||
|
const reqs = testDb.select().from(schema.friendRequests).all();
|
||||||
|
expect(reqs).toHaveLength(1);
|
||||||
|
expect(reqs[0]!.id).toBe('existing-req');
|
||||||
|
expect(reqs[0]!.createdAt).toBe(1000);
|
||||||
|
expect(sendToUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts and creates the request when only a non-pending (declined) request exists from the same sender', async () => {
|
||||||
|
// Idempotency check is gated on status='pending', so a prior declined request must NOT block a fresh one.
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
seedReplicatedUser({
|
||||||
|
id: 'bob-stub',
|
||||||
|
username: 'remote-bob@orbit.test',
|
||||||
|
homeUserId: 'remote-bob',
|
||||||
|
homeInstance: 'orbit.test',
|
||||||
|
});
|
||||||
|
testDb.insert(schema.friendRequests).values({
|
||||||
|
id: 'old-declined',
|
||||||
|
fromId: 'bob-stub',
|
||||||
|
toId: 'alice-id',
|
||||||
|
status: 'declined',
|
||||||
|
createdAt: 1000,
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'after-decline',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
createdAt: 5000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual(['after-decline']);
|
||||||
|
expect(result.rejected).toEqual([]);
|
||||||
|
const pending = testDb.select().from(schema.friendRequests)
|
||||||
|
.where(and(
|
||||||
|
eq(schema.friendRequests.fromId, 'bob-stub'),
|
||||||
|
eq(schema.friendRequests.toId, 'alice-id'),
|
||||||
|
eq(schema.friendRequests.status, 'pending'),
|
||||||
|
)).all();
|
||||||
|
expect(pending).toHaveLength(1);
|
||||||
|
expect(pending[0]!.createdAt).toBe(5000);
|
||||||
|
expect(sendToUser).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses a pre-existing replicated sender on the happy path (no new stub)', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
seedReplicatedUser({
|
||||||
|
id: 'bob-stub',
|
||||||
|
username: 'remote-bob@orbit.test',
|
||||||
|
homeUserId: 'remote-bob',
|
||||||
|
homeInstance: 'orbit.test',
|
||||||
|
});
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'reuse-stub',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual(['reuse-stub']);
|
||||||
|
const stubs = testDb.select().from(schema.users)
|
||||||
|
.where(eq(schema.users.homeUserId, 'remote-bob')).all();
|
||||||
|
expect(stubs).toHaveLength(1);
|
||||||
|
expect(stubs[0]!.id).toBe('bob-stub');
|
||||||
|
|
||||||
|
const reqs = testDb.select().from(schema.friendRequests).all();
|
||||||
|
expect(reqs).toHaveLength(1);
|
||||||
|
expect(reqs[0]!.fromId).toBe('bob-stub');
|
||||||
|
expect(reqs[0]!.toId).toBe('alice-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hydrates profile snapshot fields onto the sender stub on creation', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'hydrate-profile',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: {
|
||||||
|
username: 'bob',
|
||||||
|
displayName: 'Bob the Builder',
|
||||||
|
avatar: 'avatar-bob.png',
|
||||||
|
avatarColor: 'mint',
|
||||||
|
banner: 'banner-bob.png',
|
||||||
|
bio: 'I build things.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual(['hydrate-profile']);
|
||||||
|
const stub = testDb.select().from(schema.users)
|
||||||
|
.where(eq(schema.users.homeUserId, 'remote-bob')).get();
|
||||||
|
expect(stub).toBeDefined();
|
||||||
|
expect(stub!.displayName).toBe('Bob the Builder');
|
||||||
|
// Bare filenames must be resolved against the sender's home origin.
|
||||||
|
expect(stub!.avatar).toBe('https://orbit.test/api/uploads/avatar-bob.png');
|
||||||
|
expect(stub!.banner).toBe('https://orbit.test/api/uploads/banner-bob.png');
|
||||||
|
expect(stub!.avatarColor).toBe('mint');
|
||||||
|
expect(stub!.bio).toBe('I build things.');
|
||||||
|
|
||||||
|
// The WS broadcast carries the hydrated profile.
|
||||||
|
const [, evt] = sendToUser.mock.calls[0]!;
|
||||||
|
expect(evt.request.user?.displayName).toBe('Bob the Builder');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses event.friendship.createdAt for the friend_request row when provided', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'explicit-created-at',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
createdAt: 4242,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
const req = testDb.select().from(schema.friendRequests).get();
|
||||||
|
expect(req?.createdAt).toBe(4242);
|
||||||
|
const [, evt] = sendToUser.mock.calls[0]!;
|
||||||
|
expect(evt.request.createdAt).toBe(4242);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to a fresh timestamp when friendship.createdAt is missing', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
const before = Date.now();
|
||||||
|
|
||||||
|
const event = makeEvent({
|
||||||
|
messageId: 'no-created-at',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Strip the default we set in makeEvent so the handler exercises its fallback.
|
||||||
|
delete (event.friendship as { createdAt?: number }).createdAt;
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
await processRelayEvents([event], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
const req = testDb.select().from(schema.friendRequests).get();
|
||||||
|
expect(req?.createdAt).toBeGreaterThanOrEqual(before);
|
||||||
|
expect(req?.createdAt).toBeLessThanOrEqual(Date.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isolates per-event success/failure within a batch (mixed accepted/rejected)', async () => {
|
||||||
|
seedLocalUser('alice-id', 'alice');
|
||||||
|
|
||||||
|
const okEvent = makeEvent({
|
||||||
|
messageId: 'batch-ok',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-bob', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'alice-id', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'bob' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const badEvent = makeEvent({
|
||||||
|
messageId: 'batch-bad',
|
||||||
|
friendship: {
|
||||||
|
from: { homeUserId: 'remote-eve', homeInstance: 'https://orbit.test' },
|
||||||
|
to: { homeUserId: 'ghost', homeInstance: 'https://home.test' },
|
||||||
|
fromProfile: { username: 'eve' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { processRelayEvents } = await import('./federation.js');
|
||||||
|
const result = await processRelayEvents([okEvent, badEvent], 'https://orbit.test', 'https://orbit.test', testDb);
|
||||||
|
|
||||||
|
expect(result.accepted).toEqual(['batch-ok']);
|
||||||
|
expect(result.rejected).toEqual([{ messageId: 'batch-bad', reason: 'recipient_not_found' }]);
|
||||||
|
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user