Skip to content
Closed
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
163 changes: 163 additions & 0 deletions backend/__tests__/unit/routes/slackOAuth.confirmRace.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// A real Mongo row catches gate updates that a mocked findOneAndUpdate cannot.
jest.mock('jsonwebtoken', () => ({}));
jest.mock('../../../middleware/auth', () => (req, _res, next) => {
req.user = { id: '64b64c48c4f37a6b2f34c111' };
next();
});
jest.mock('../../../middleware/integrationRateLimit', () => ({
writeIntegrationsRateLimit: (_req, _res, next) => next(),
listIntegrationsRateLimit: (_req, _res, next) => next(),
}));
jest.mock('../../../models/InstallableInstallation', () => ({ findOne: jest.fn() }));
jest.mock('../../../services/connectorSecrets', () => ({ get: jest.fn() }));
jest.mock('../../../services/slackOAuthService', () => ({
SlackOAuthConfigurationError: class extends Error {},
SlackOAuthExchangeError: class extends Error {},
}));
jest.mock('../../../services/installable/installableInstallationService', () => ({
install: jest.fn(), uninstall: jest.fn(),
InstallLockLostError: class extends Error {},
InstallableAlreadyInstalledError: class extends Error {},
InstallableNotFoundError: class extends Error {},
InstallableProjectionError: class extends Error {},
InstallInProgressError: class extends Error {},
InstallationPausedError: class extends Error {},
}));
jest.mock('../../../services/connectorDeliveryFailureService', () => ({
noteBoundChatDeliveryFailure: jest.fn().mockResolvedValue(false),
}));
jest.mock('../../../services/slackApi', () => {
const actual = jest.requireActual('../../../services/slackApi');
const postMessage = jest.fn().mockResolvedValue({ ok: true, ts: '1.1' });
const SlackApi = jest.fn().mockImplementation(() => ({ postMessage }));
SlackApi.escapeSlackMrkdwn = actual.escapeSlackMrkdwn;
SlackApi.postMessage = postMessage;
return SlackApi;
});

const express = require('express');
const request = require('supertest');
const mongoose = require('mongoose');
const Integration = require('../../../models/Integration');
const Pod = require('../../../models/Pod');
const User = require('../../../models/User');
const InstallableInstallation = require('../../../models/InstallableInstallation');
const connectorSecrets = require('../../../services/connectorSecrets');
const SlackApi = require('../../../services/slackApi');
const { relayAgentMessageToSlack } = require('../../../services/slackBridgeService');
const { setupMongoDb, closeMongoDb, clearMongoDb } = require('../../utils/testUtils');
const routes = require('../../../routes/installables');

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

const ownerId = new mongoose.Types.ObjectId('64b64c48c4f37a6b2f34c111');
const botId = new mongoose.Types.ObjectId();

describe('Slack lead confirmation gate race', () => {
beforeAll(async () => {
await setupMongoDb();
await Integration.syncIndexes();
});
afterEach(async () => {
jest.restoreAllMocks();
SlackApi.postMessage.mockClear();
await clearMongoDb();
});
afterAll(async () => closeMongoDb());

const setup = async (gate) => {
await User.create([
{ _id: ownerId, username: 'owner', email: 'owner@example.test', password: 'Password123!' },
{ _id: botId, username: 'kai', email: 'kai@example.test', password: 'Password123!', isBot: true },
]);
const pod = await Pod.create({
name: 'Team', type: 'team', createdBy: ownerId, members: [ownerId, botId],
});
const installationId = new mongoose.Types.ObjectId().toString();
InstallableInstallation.findOne.mockResolvedValue({ _id: installationId });
connectorSecrets.get.mockResolvedValue('xoxb-test');
const integration = await Integration.create({
installationId, type: 'slack', scope: 'user', status: 'pending',
podId: pod._id, createdBy: ownerId, isActive: true,
config: {
pendingBind: {
teamId: 'T1', chatId: 'D1', slackUserId: 'U1',
botTokenRef: 'secret-ref', expiresAt: new Date(Date.now() + 60_000),
},
...(gate ? { gates: { [String(pod._id)]: gate } } : {}),
},
});
return { pod, integration };
};

it.each([
[{ enabled: false, mode: 'attention' }],
[{ enabled: true, mode: 'attention' }],
])('preserves a concurrent owner gate change and does not relay an ordinary message', async (ownerGate) => {
const { pod, integration } = await setup({
enabled: true, mode: 'mirror', since: new Date('2026-10-01'),
});
const write = Integration.findOneAndUpdate.bind(Integration);
jest.spyOn(Integration, 'findOneAndUpdate').mockImplementationOnce(async (...args) => {
await Integration.updateOne(
{ _id: integration._id },
{ $set: {
[`config.gates.${pod._id}.enabled`]: ownerGate.enabled,
[`config.gates.${pod._id}.mode`]: ownerGate.mode,
} },
);
return write(...args);
});

const response = await request(app).post('/api/installables/slack/confirm')
.send({ leadAgentUsername: 'kai' });
expect(response.status).toBe(200);
const stored = await Integration.findById(integration._id).lean();
expect(stored.config.gates[String(pod._id)]).toMatchObject({ ...ownerGate, lead: 'kai' });

SlackApi.postMessage.mockClear(); // Ignore the intentional connect marker.
await relayAgentMessageToSlack({
podId: String(pod._id), agentUsername: 'other', displayName: 'Other',
content: 'ordinary pod message', integration: stored,
});
expect(SlackApi.postMessage).not.toHaveBeenCalled();
});

it('does not overwrite a gate created while the bind was being confirmed', async () => {
const { pod, integration } = await setup(null);
const write = Integration.findOneAndUpdate.bind(Integration);
jest.spyOn(Integration, 'findOneAndUpdate').mockImplementationOnce(async (...args) => {
await Integration.updateOne(
{ _id: integration._id },
{ $set: {
[`config.gates.${pod._id}`]: {
enabled: false, mode: 'attention', since: new Date(),
},
} },
);
return write(...args);
});

const response = await request(app).post('/api/installables/slack/confirm')
.send({ leadAgentUsername: 'kai' });
expect(response.status).toBe(409);
const stored = await Integration.findById(integration._id).lean();
expect(stored.config.gates[String(pod._id)]).toMatchObject({
enabled: false, mode: 'attention',
});
expect(stored.config.chatId).toBeUndefined();
});

it('initializes a missing gate when nobody changed it during confirmation', async () => {
const { pod, integration } = await setup(null);
const response = await request(app).post('/api/installables/slack/confirm')
.send({ leadAgentUsername: 'kai' });
expect(response.status).toBe(200);
const stored = await Integration.findById(integration._id).lean();
expect(stored.config.gates[String(pod._id)]).toMatchObject({
enabled: true, lead: 'kai', since: expect.any(Date),
});
});
});
50 changes: 49 additions & 1 deletion backend/__tests__/unit/routes/slackOAuth.installables.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jest.mock('../../../middleware/integrationRateLimit', () => ({
listIntegrationsRateLimit: (_req, _res, next) => next(),
}));
jest.mock('../../../models/Pod', () => ({ findById: jest.fn() }));
jest.mock('../../../models/User', () => ({ findById: jest.fn() }));
jest.mock('../../../models/User', () => ({ findById: jest.fn(), findOne: jest.fn() }));
jest.mock('../../../models/Integration', () => ({
findOne: jest.fn(), findOneAndUpdate: jest.fn(), findById: jest.fn(), updateOne: jest.fn(),
collection: { indexExists: jest.fn() },
Expand Down Expand Up @@ -294,6 +294,54 @@ describe('Slack installable OAuth routes', () => {
expect(SlackApi.mock.results[0].value.postMessage).toHaveBeenCalledWith('D1', '[<Evil|pod>] connected');
});

test('stores the selected lead on the active pod gate at confirmation', async () => {
const pending = {
teamId: 'T1', slackUserId: 'U1', chatId: 'D1', botTokenRef: 'secret-ref',
expiresAt: new Date(Date.now() + 60_000),
};
const since = new Date('2026-10-01T00:00:00.000Z');
Integration.findOne.mockResolvedValue({
...integration,
config: { pendingBind: pending, gates: { [integration.podId]: { enabled: false, mode: 'attention', since } } },
});
Pod.findById
.mockResolvedValueOnce({ members: [ownerId, 'bot-1'], type: 'team' })
.mockReturnValueOnce({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue({ name: 'Pod' }) }) });
User.findOne.mockResolvedValue({ _id: 'bot-1', username: 'kai', isBot: true });
Integration.findOneAndUpdate.mockResolvedValue({ ...integration, status: 'connected', config: { ...pending, chatType: 'im' } });
SlackApi.mockImplementationOnce(() => ({ postMessage: jest.fn().mockResolvedValue({ ok: true, ts: '1.1' }) }));

const response = await request(app).post('/api/installables/slack/confirm')
.send({ leadAgentUsername: 'kai' });

expect(response.status).toBe(200);
expect(User.findOne).toHaveBeenCalledWith({
_id: { $in: [ownerId, 'bot-1'] }, username: 'kai', isBot: true,
});
expect(Integration.findOneAndUpdate.mock.calls[0][1].$set[
`config.gates.${integration.podId}.lead`
]).toBe('kai');
expect(Integration.findOneAndUpdate.mock.calls[0][1].$set[
`config.gates.${integration.podId}`
]).toBeUndefined();
});

test('refuses a selected lead who is not a bot member without binding the DM', async () => {
Integration.findOne.mockResolvedValue({
...integration,
config: { pendingBind: { teamId: 'T1', chatId: 'D1', botTokenRef: 'secret-ref', expiresAt: new Date(Date.now() + 60_000) } },
});
Pod.findById.mockResolvedValue({ members: [ownerId], type: 'team' });
User.findOne.mockResolvedValue(null);

const response = await request(app).post('/api/installables/slack/confirm')
.send({ leadAgentUsername: 'outsider' });

expect(response.status).toBe(400);
expect(response.body.code).toBe('slack_lead_invalid');
expect(Integration.findOneAndUpdate).not.toHaveBeenCalled();
});

test('refuses a Slack DM already bound to another Commonly account by name', async () => {
const pending = {
teamId: 'T1', slackUserId: 'U1', chatId: 'D1', botTokenRef: 'secret-ref',
Expand Down
132 changes: 129 additions & 3 deletions backend/__tests__/unit/services/slackBridgeService.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
jest.mock('../../../models/Integration', () => ({ findOne: jest.fn(), findByIdAndUpdate: jest.fn() }));
jest.mock('../../../models/Pod', () => ({ findById: jest.fn() }));
jest.mock('../../../models/User', () => ({ findById: jest.fn() }));
jest.mock('../../../models/User', () => ({ findById: jest.fn(), findOne: jest.fn() }));
jest.mock('../../../models/pg/Message', () => ({ create: jest.fn(), findById: jest.fn() }));
jest.mock('../../../services/messageAgentDeliveryService', () => ({ deliverMessageToAgents: jest.fn() }));
jest.mock('../../../config/socket', () => ({ getIO: jest.fn(() => null) }));
Expand Down Expand Up @@ -142,9 +142,9 @@ describe('Slack installable bridge', () => {

// Two fixture shapes, because the routed pod and the active pod are different
// documents: the first names the pod that may not be reachable any more.
const podWithMembers = (name) => ({
const podWithMembers = (name, members = ['user-1']) => ({
select: jest.fn().mockReturnValue({
lean: jest.fn().mockResolvedValue({ name, type: 'team', members: ['user-1'] }),
lean: jest.fn().mockResolvedValue({ name, type: 'team', members }),
}),
});
const podsById = (gatedPodName) => (id) => ({
Expand Down Expand Up @@ -211,6 +211,132 @@ describe('Slack installable bridge', () => {
expect(PGMessage.create.mock.calls[0][0]).toBe('pod-1');
});

test('addresses an unquoted plain message to the active pod lead', async () => {
Pod.findById.mockReturnValue(podWithMembers('Alpha', ['user-1', 'bot-1']));
User.findOne.mockResolvedValue({ _id: 'bot-1', username: 'kai', isBot: true });
PGMessage.create.mockImplementation((_podId, _userId, content) => Promise.resolve({ id: 'pg-1', content }));
PGMessage.findById.mockResolvedValue(null);
User.findById.mockReturnValue({
select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue({ username: 'sam' }) }),
});
deliverMessageToAgents.mockResolvedValue(undefined);

const result = await relaySlackMessageToPod({
integration: {
...integration,
scope: 'user',
config: { ...integration.config, linkedUserId: 'user-1', slackUserId: 'U1', gates: { 'pod-1': { enabled: true, lead: 'kai' } } },
},
event: { text: 'hello', user: 'U1' },
});

expect(result).toEqual({ relayed: true, routedAgent: 'kai' });
expect(PGMessage.create.mock.calls[0][2]).toBe('💬 (via Slack): @kai hello');
expect(User.findOne).toHaveBeenCalledWith({
_id: { $in: ['user-1', 'bot-1'] }, username: 'kai', isBot: true,
});
expect(deliverMessageToAgents.mock.calls[0][0].message).toEqual(expect.objectContaining({
id: 'pg-1', content: '💬 (via Slack): @kai hello',
}));
expect(SlackApi).not.toHaveBeenCalled();
});

test('keeps an explicit mention instead of substituting the selected lead', async () => {
Pod.findById.mockReturnValue(podWithMembers('Alpha'));
PGMessage.create.mockResolvedValue({ id: 'pg-1' });
PGMessage.findById.mockResolvedValue({ id: 'pg-1', content: 'relayed' });
User.findById.mockReturnValue({
select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue({ username: 'sam' }) }),
});
deliverMessageToAgents.mockResolvedValue(undefined);

await relaySlackMessageToPod({
integration: {
...integration,
scope: 'user',
config: { ...integration.config, linkedUserId: 'user-1', slackUserId: 'U1', gates: { 'pod-1': { enabled: true, lead: 'kai' } } },
},
event: { text: '@vera please review', user: 'U1' },
});

expect(PGMessage.create.mock.calls[0][2]).toBe('💬 (via Slack): @vera please review');
expect(SlackApi).not.toHaveBeenCalled();
});

test('acknowledges an unaddressed message when the active pod has no lead', async () => {
Pod.findById.mockReturnValue(podWithMembers('Alpha'));
PGMessage.create.mockResolvedValue({ id: 'pg-1' });
PGMessage.findById.mockResolvedValue({ id: 'pg-1', content: 'relayed' });
User.findById.mockReturnValue({
select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue({ username: 'sam' }) }),
});
deliverMessageToAgents.mockResolvedValue(undefined);

await relaySlackMessageToPod({
integration: {
...integration,
scope: 'user',
config: { ...integration.config, linkedUserId: 'user-1', slackUserId: 'U1', gates: {} },
},
event: { text: 'hello', user: 'U1' },
});

expect(PGMessage.create.mock.calls[0][2]).toBe('💬 (via Slack): hello');
await new Promise((resolve) => setImmediate(resolve));
expect(SlackApi.mock.results[0].value.postMessage).toHaveBeenCalledWith(
'D1', expect.stringContaining('no lead agent is selected'),
);
});

test('a stalled no-lead Slack notice cannot hold pod delivery', async () => {
Pod.findById.mockReturnValue(podWithMembers('Alpha'));
PGMessage.create.mockResolvedValue({ id: 'pg-1' });
PGMessage.findById.mockResolvedValue({ id: 'pg-1', content: 'relayed' });
User.findById.mockReturnValue({
select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue({ username: 'sam' }) }),
});
deliverMessageToAgents.mockResolvedValue(undefined);
SlackApi.mockImplementationOnce(() => ({ postMessage: jest.fn(() => new Promise(() => {})) }));

const result = await relaySlackMessageToPod({
integration: {
...integration,
scope: 'user',
config: { ...integration.config, linkedUserId: 'user-1', slackUserId: 'U1', gates: {} },
},
event: { text: 'hello', user: 'U1' },
});

expect(result.relayed).toBe(true);
expect(deliverMessageToAgents).toHaveBeenCalledTimes(1);
});

test('does not wake a selected lead after that bot leaves the pod', async () => {
Pod.findById.mockReturnValue(podWithMembers('Alpha'));
User.findOne.mockResolvedValue(null);
PGMessage.create.mockResolvedValue({ id: 'pg-1' });
PGMessage.findById.mockResolvedValue({ id: 'pg-1', content: 'relayed' });
User.findById.mockReturnValue({
select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue({ username: 'sam' }) }),
});
deliverMessageToAgents.mockResolvedValue(undefined);

await relaySlackMessageToPod({
integration: {
...integration,
scope: 'user',
config: { ...integration.config, linkedUserId: 'user-1', slackUserId: 'U1', gates: { 'pod-1': { enabled: true, lead: 'kai' } } },
},
event: { text: 'hello', user: 'U1' },
});

expect(PGMessage.create.mock.calls[0][2]).toBe('💬 (via Slack): hello');
await new Promise((resolve) => setImmediate(resolve));
expect(SlackApi.mock.results[0].value.postMessage).toHaveBeenCalledWith(
'D1', expect.stringContaining('no lead agent is selected'),
);
});

test('refuses a thread reply into a pod whose gate is off, naming it, posting nothing', async () => {
Pod.findById.mockImplementation(podsById('Launch'));

Expand Down
Loading
Loading