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
15 changes: 15 additions & 0 deletions backend/__tests__/unit/routes/discord.management-auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,19 @@ describe('discord management route auth', () => {
expect(res.status).toBe(403);
expect(DiscordIntegration.findOneAndDelete).not.toHaveBeenCalled();
});

it('scopes legacy uninstall lookup to Discord integrations', async () => {
Integration.findOne.mockResolvedValue(null);

const res = await request(app)
.delete('/api/discord/uninstall/install-1')
.set('Authorization', 'Bearer user-token');

expect(res.status).toBe(404);
expect(Integration.findOne).toHaveBeenCalledWith({
installationId: 'install-1',
type: 'discord',
});
expect(Integration.findByIdAndDelete).not.toHaveBeenCalled();
});
});
189 changes: 189 additions & 0 deletions backend/__tests__/unit/routes/installables.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
const request = require('supertest');
const express = require('express');

jest.mock('../../../middleware/auth', () => (req, res, next) => {
if (!req.header('Authorization')) return res.status(401).json({ error: 'Unauthorized' });
req.user = { id: '64b64c48c4f37a6b2f34c111' };
return next();
});

jest.mock('../../../middleware/integrationRateLimit', () => ({
writeIntegrationsRateLimit: (_req, _res, next) => next(),
}));

jest.mock('../../../models/Pod', () => ({ findById: jest.fn() }));
jest.mock('../../../services/installable/installableInstallationService', () => ({
install: jest.fn(),
uninstall: jest.fn(),
InstallLockLostError: class InstallLockLostError extends Error {},
InstallableAlreadyInstalledError: function InstallableAlreadyInstalledError(boundPodId) {
this.message = 'This connector is already installed for another pod.';
this.boundPodId = boundPodId;
},
InstallableNotFoundError: class InstallableNotFoundError extends Error {},
InstallableProjectionError: class InstallableProjectionError extends Error {},
InstallInProgressError: function InstallInProgressError(boundPodId) {
this.message = 'This install is still in progress; try again shortly.';
this.boundPodId = boundPodId;
},
}));

const Pod = require('../../../models/Pod');
const installationService = require('../../../services/installable/installableInstallationService');
const installableRoutes = require('../../../routes/installables');

const app = express();
app.use(express.json());
app.use('/api/installables', installableRoutes);

const auth = { Authorization: 'Bearer test-token' };
const podId = '64b64c48c4f37a6b2f34c222';

describe('installable connector routes', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('rejects a non-member before any install row is claimed', async () => {
Pod.findById.mockResolvedValue({
_id: podId,
createdBy: { toString: () => 'someone-else' },
members: ['someone-else'],
});

const res = await request(app)
.post('/api/installables/telegram/install')
.set(auth)
.send({ podId });

expect(res.status).toBe(403);
expect(installationService.install).not.toHaveBeenCalled();
});

it('rejects an invalid podId without querying a pod or claiming an install', async () => {
const response = await request(app)
.post('/api/installables/telegram/install')
.set('Authorization', 'Bearer valid')
.send({ podId: 'not-an-object-id' });

expect(response.status).toBe(400);
expect(response.body.error).toBe('podId must be a valid ObjectId');
expect(Pod.findById).not.toHaveBeenCalled();
expect(installationService.install).not.toHaveBeenCalled();
});

it('derives the install target from auth and accepts only the selected pod', async () => {
Pod.findById.mockResolvedValue({
_id: podId,
createdBy: { toString: () => 'someone-else' },
members: ['64b64c48c4f37a6b2f34c111'],
});
installationService.install.mockResolvedValue({
httpStatus: 200,
state: 'active',
installation: { _id: 'install-1' },
integration: { _id: 'integration-1' },
});

const res = await request(app)
.post('/api/installables/telegram/install')
.set(auth)
.send({ podId });

expect(res.status).toBe(200);
expect(installationService.install).toHaveBeenCalledWith({
installableId: 'telegram',
installedBy: '64b64c48c4f37a6b2f34c111',
podId,
});
});

it('returns typed 409 when activation loses its projection', async () => {
Pod.findById.mockResolvedValue({
_id: podId,
createdBy: { toString: () => 'someone-else' },
members: ['64b64c48c4f37a6b2f34c111'],
});
installationService.install.mockRejectedValue(
new installationService.InstallLockLostError('install lock lost'),
);

const res = await request(app)
.post('/api/installables/telegram/install')
.set(auth)
.send({ podId });

expect(res.status).toBe(409);
expect(res.body.code).toBe('install_lock_lost');
});

it('returns the bound pod when an active connector is requested for another pod', async () => {
Pod.findById.mockResolvedValue({
_id: podId,
createdBy: { toString: () => 'someone-else' },
members: ['64b64c48c4f37a6b2f34c111'],
});
installationService.install.mockRejectedValue(
new installationService.InstallableAlreadyInstalledError('64b64c48c4f37a6b2f34c333'),
);

const res = await request(app)
.post('/api/installables/telegram/install')
.set(auth)
.send({ podId });

expect(res.status).toBe(409);
expect(res.body).toMatchObject({
code: 'already_installed',
boundPodId: '64b64c48c4f37a6b2f34c333',
});
});

it('returns the pending pod when a fresh claim targets another pod', async () => {
Pod.findById.mockResolvedValue({
_id: podId,
createdBy: { toString: () => 'someone-else' },
members: ['64b64c48c4f37a6b2f34c111'],
});
installationService.install.mockRejectedValue(
new installationService.InstallInProgressError('64b64c48c4f37a6b2f34c333'),
);

const res = await request(app)
.post('/api/installables/telegram/install')
.set(auth)
.send({ podId });

expect(res.status).toBe(409);
expect(res.body).toMatchObject({
code: 'install_in_progress',
boundPodId: '64b64c48c4f37a6b2f34c333',
});
});

it('cannot use an uninstall body to target another user installation', async () => {
installationService.uninstall.mockResolvedValue({ _id: 'install-1', status: 'uninstalled' });

const res = await request(app)
.delete('/api/installables/telegram/install')
.set(auth)
.send({ installationId: 'another-users-install' });

expect(res.status).toBe(200);
expect(installationService.uninstall).toHaveBeenCalledWith({
installableId: 'telegram',
installedBy: '64b64c48c4f37a6b2f34c111',
});
});

it('does not report a concurrent revocation as disconnected', async () => {
installationService.uninstall.mockResolvedValue({ _id: 'install-1', status: 'uninstalling' });

const res = await request(app)
.delete('/api/installables/telegram/install')
.set(auth);

expect(res.status).toBe(202);
expect(res.body.status).toBe('uninstalling');
});
});
169 changes: 169 additions & 0 deletions backend/__tests__/unit/services/installableEventHandlers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// @ts-nocheck

const mongoose = require('mongoose');

const Installable = require('../../../models/Installable');
const InstallableInstallation = require('../../../models/InstallableInstallation');
const Integration = require('../../../models/Integration');
const {
install,
} = require('../../../services/installable/installableInstallationService');
const {
dispatch,
eventHandlers,
} = require('../../../services/installable/eventHandlers');
const telegramSend = require('../../../services/telegramService');
const { TELEGRAM_CONNECTOR } = require('../../../scripts/seed-builtin-connectors');
const {
setupMongoDb,
closeMongoDb,
clearMongoDb,
} = require('../../utils/testUtils');

const freshId = () => new mongoose.Types.ObjectId().toString();

describe('installable event dispatcher', () => {
let originalTelegramHandler;

beforeAll(async () => {
await setupMongoDb();
await InstallableInstallation.syncIndexes();
});

afterAll(async () => {
await closeMongoDb();
});

beforeEach(async () => {
await clearMongoDb();
await Installable.create({
...TELEGRAM_CONNECTOR,
stats: { totalInstalls: 0, activeInstalls: 0, forkCount: 0 },
});
originalTelegramHandler = eventHandlers['telegram.relay'];
});

afterEach(() => {
eventHandlers['telegram.relay'] = originalTelegramHandler;
});

it('selects only the event pod connector before invoking its handler', async () => {
const podA = freshId();
const podB = freshId();
await install({ installableId: 'telegram', installedBy: freshId(), podId: podA });
await install({ installableId: 'telegram', installedBy: freshId(), podId: podB });
const relay = jest.fn().mockResolvedValue(undefined);
eventHandlers['telegram.relay'] = relay;

await dispatch('chat.message', {
podId: podA,
agentUsername: 'kai',
displayName: 'Kai',
content: '[ESCALATE] needs review',
podMessageId: 'message-a',
});

expect(relay).toHaveBeenCalledTimes(1);
expect(relay).toHaveBeenCalledWith(expect.objectContaining({
podId: podA,
agentUsername: 'kai',
displayName: 'Kai',
content: '[ESCALATE] needs review',
podMessageId: 'message-a',
integration: expect.objectContaining({ podId: expect.anything() }),
}));
expect(String(relay.mock.calls[0][0].integration.podId)).toBe(podA);
});

it('relays each same-pod connector to its own selected Telegram chat', async () => {
const podId = freshId();
const first = await install({ installableId: 'telegram', installedBy: freshId(), podId });
const second = await install({ installableId: 'telegram', installedBy: freshId(), podId });
await Integration.updateOne(
{ _id: first.integration._id },
{ $set: { 'config.chatId': 'chat-a', 'config.chatType': 'private' } },
);
await Integration.updateOne(
{ _id: second.integration._id },
{ $set: { 'config.chatId': 'chat-b', 'config.chatType': 'private' } },
);
const previousToken = process.env.TELEGRAM_BOT_TOKEN;
process.env.TELEGRAM_BOT_TOKEN = 'test-bot-token';
const sendMessage = jest.spyOn(telegramSend, 'sendMessage')
.mockResolvedValueOnce({ messageId: 1 })
.mockResolvedValueOnce({ messageId: 2 });

try {
await dispatch('chat.message', {
podId,
agentUsername: 'kai',
displayName: 'Kai',
content: 'Both subscriptions should receive this',
podMessageId: 'message-fanout',
});

expect(sendMessage).toHaveBeenCalledTimes(2);
expect(new Set(sendMessage.mock.calls.map((call) => call[1]))).toEqual(
new Set(['chat-a', 'chat-b']),
);
} finally {
sendMessage.mockRestore();
if (previousToken === undefined) delete process.env.TELEGRAM_BOT_TOKEN;
else process.env.TELEGRAM_BOT_TOKEN = previousToken;
}
});

it('does not invoke a handler when the event pod has no installation', async () => {
const relay = jest.fn().mockResolvedValue(undefined);
eventHandlers['telegram.relay'] = relay;

await dispatch('chat.message', {
podId: freshId(),
agentUsername: 'kai',
displayName: 'Kai',
content: 'No connector',
podMessageId: 'message-none',
});

expect(relay).not.toHaveBeenCalled();
});

it('continues to dispatch an existing direct Telegram integration', async () => {
const podId = freshId();
await Integration.create({
podId,
type: 'telegram',
status: 'pending',
createdBy: freshId(),
isActive: true,
config: { liveRelay: true, chatId: 'chat-1', chatType: 'private' },
});
const relay = jest.fn().mockResolvedValue(undefined);
eventHandlers['telegram.relay'] = relay;

await dispatch('chat.message', {
podId,
agentUsername: 'kai',
displayName: 'Kai',
content: 'Legacy connector still works',
podMessageId: 'message-legacy',
});

expect(relay).toHaveBeenCalledTimes(1);
expect(relay.mock.calls[0][0].podId).toBe(podId);
});

it('contains an individual handler failure', async () => {
const podId = freshId();
await install({ installableId: 'telegram', installedBy: freshId(), podId });
eventHandlers['telegram.relay'] = jest.fn().mockRejectedValue(new Error('provider unavailable'));

await expect(dispatch('chat.message', {
podId,
agentUsername: 'kai',
displayName: 'Kai',
content: 'Will not fail the post',
podMessageId: 'message-fail',
})).resolves.toBeUndefined();
});
});
Loading
Loading