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
27 changes: 27 additions & 0 deletions backend/__tests__/unit/models/Integration.serialization.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const mongoose = require('mongoose');
const Integration = require('../../../models/Integration');

describe('Integration JSON serialization', () => {
it('exposes a pause reason and time without the moderator identity', () => {
const pausedAt = new Date('2026-09-05T08:48:00.000Z');
const integration = new Integration({
type: 'telegram',
createdBy: new mongoose.Types.ObjectId(),
config: {
adminPause: {
reason: 'Safety review in progress.',
at: pausedAt,
adminId: 'admin-private-id',
},
},
});

const body = integration.toJSON();

expect(body.config.adminPause).toEqual({
reason: 'Safety review in progress.',
at: pausedAt,
});
expect(JSON.stringify(body)).not.toMatch(/adminId|admin-private-id/);
});
});
127 changes: 127 additions & 0 deletions backend/__tests__/unit/routes/admin.installables.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const request = require('supertest');
const express = require('express');

jest.mock('../../../middleware/auth', () => (req, _res, next) => {
req.user = { id: req.header('x-test-user') || 'admin-1' };
next();
});
jest.mock('../../../middleware/adminAuth', () => (req, res, next) => {
if (req.header('x-test-admin') !== 'true') return res.status(403).json({ error: 'Admin access required' });
return next();
});
jest.mock('../../../models/InstallableInstallation', () => ({
findOne: jest.fn(),
findOneAndUpdate: jest.fn(),
}));
jest.mock('../../../models/Integration', () => ({ updateMany: jest.fn() }));

const InstallableInstallation = require('../../../models/InstallableInstallation');
const Integration = require('../../../models/Integration');
const adminInstallableRoutes = require('../../../routes/admin/installables');

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

const installableId = 'telegram';
const installationId = '64b64c48c4f37a6b2f34c222';
const admin = { 'x-test-admin': 'true' };
const active = {
_id: installationId,
installableId,
targetType: 'user',
scope: 'user',
status: 'active',
installedBy: 'owner-1',
};

describe('admin installable pause/resume', () => {
beforeEach(() => jest.clearAllMocks());

it('requires the admin middleware before it can stop a private connector', async () => {
const res = await request(app)
.post(`/api/admin/installables/${installableId}/installations/${installationId}/pause`)
.send({ reason: 'Safety review' });

expect(res.status).toBe(403);
expect(InstallableInstallation.findOneAndUpdate).not.toHaveBeenCalled();
});

it('requires a reason before any parent or projection write', async () => {
const res = await request(app)
.post(`/api/admin/installables/${installableId}/installations/${installationId}/pause`)
.set(admin)
.send({});

expect(res.status).toBe(400);
expect(InstallableInstallation.findOne).not.toHaveBeenCalled();
expect(InstallableInstallation.findOneAndUpdate).not.toHaveBeenCalled();
expect(Integration.updateMany).not.toHaveBeenCalled();
});

it('pauses the parent before stamping its inbound projection', async () => {
const paused = { ...active, status: 'paused' };
InstallableInstallation.findOne.mockResolvedValue(active);
InstallableInstallation.findOneAndUpdate.mockResolvedValue(paused);
Integration.updateMany.mockResolvedValue({ modifiedCount: 1 });

const res = await request(app)
.post(`/api/admin/installables/${installableId}/installations/${installationId}/pause`)
.set(admin)
.send({ reason: 'Safety review' });

expect(res.status).toBe(200);
expect(res.body).toMatchObject({ status: 'paused', projected: true });
expect(InstallableInstallation.findOneAndUpdate).toHaveBeenCalledWith(
expect.objectContaining({ status: 'active', targetType: 'user', scope: 'user' }),
expect.objectContaining({
$set: expect.objectContaining({ status: 'paused', adminPause: expect.objectContaining({ reason: 'Safety review' }) }),
$push: expect.objectContaining({ pauseAudit: expect.objectContaining({ action: 'pause', ownerId: 'owner-1' }) }),
}),
{ new: true },
);
expect(Integration.updateMany).toHaveBeenCalledWith(
{ installationId },
expect.objectContaining({ $set: expect.objectContaining({ 'config.adminPause': expect.objectContaining({ reason: 'Safety review' }) }) }),
);
});

it('reports a partial pause when child projection fails after the parent stopped', async () => {
InstallableInstallation.findOne.mockResolvedValue(active);
InstallableInstallation.findOneAndUpdate.mockResolvedValue({ ...active, status: 'paused' });
Integration.updateMany.mockRejectedValue(new Error('db unavailable'));

const res = await request(app)
.post(`/api/admin/installables/${installableId}/installations/${installationId}/pause`)
.set(admin)
.send({ reason: 'Safety review' });

expect(res.status).toBe(202);
expect(res.body).toMatchObject({ status: 'paused', projected: false });
expect(InstallableInstallation.findOneAndUpdate).toHaveBeenCalledTimes(1);
});

it('clears children before resuming and leaves the parent paused if its completion CAS loses', async () => {
const paused = { ...active, status: 'paused' };
InstallableInstallation.findOne.mockResolvedValue(paused);
Integration.updateMany.mockResolvedValue({ modifiedCount: 1 });
InstallableInstallation.findOneAndUpdate.mockResolvedValue(null);

const res = await request(app)
.post(`/api/admin/installables/${installableId}/installations/${installationId}/resume`)
.set(admin)
.send({ reason: 'Review complete' });

expect(res.status).toBe(202);
expect(res.body).toMatchObject({ status: 'paused', projected: false });
expect(Integration.updateMany).toHaveBeenCalledWith(
{ installationId },
{ $unset: { 'config.adminPause': 1 } },
);
expect(InstallableInstallation.findOneAndUpdate).toHaveBeenCalledWith(
expect.objectContaining({ status: 'paused' }),
expect.objectContaining({ $set: expect.objectContaining({ status: 'active' }) }),
{ new: true },
);
});
});
60 changes: 58 additions & 2 deletions backend/__tests__/unit/routes/installables.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ jest.mock('../../../services/installable/installableInstallationService', () =>
this.message = 'This install is still in progress; try again shortly.';
this.boundPodId = boundPodId;
},
InstallationPausedError: function InstallationPausedError(reason) {
this.message = 'This connector is paused by an administrator.';
this.reason = reason;
},
}));

const Pod = require('../../../models/Pod');
Expand Down Expand Up @@ -72,7 +76,7 @@ describe('installable connector routes', () => {
expect(installationService.install).not.toHaveBeenCalled();
});

it('derives the install target from auth and accepts only the selected pod', async () => {
it('derives the install target from auth and redacts pause administrators', async () => {
Pod.findById.mockResolvedValue({
_id: podId,
createdBy: { toString: () => 'someone-else' },
Expand All @@ -82,7 +86,16 @@ describe('installable connector routes', () => {
httpStatus: 200,
state: 'active',
installation: { _id: 'install-1' },
integration: { _id: 'integration-1' },
integration: {
_id: 'integration-1',
config: {
adminPause: {
reason: 'Safety review in progress.',
at: '2026-09-05T08:48:00.000Z',
adminId: 'admin-private-id',
},
},
},
});

const res = await request(app)
Expand All @@ -96,6 +109,11 @@ describe('installable connector routes', () => {
installedBy: '64b64c48c4f37a6b2f34c111',
podId,
});
expect(res.body.integration.config.adminPause).toEqual({
reason: 'Safety review in progress.',
at: '2026-09-05T08:48:00.000Z',
});
expect(JSON.stringify(res.body)).not.toContain('admin-private-id');
});

it('returns typed 409 when activation loses its projection', async () => {
Expand Down Expand Up @@ -186,4 +204,42 @@ describe('installable connector routes', () => {
expect(res.status).toBe(202);
expect(res.body.status).toBe('uninstalling');
});

it('returns the paused refusal for install without claiming a replacement', async () => {
Pod.findById.mockResolvedValue({
_id: podId,
createdBy: { toString: () => 'someone-else' },
members: ['64b64c48c4f37a6b2f34c111'],
});
installationService.install.mockRejectedValue(
new installationService.InstallationPausedError('Safety review in progress.'),
);

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

expect(res.status).toBe(409);
expect(res.body).toMatchObject({
code: 'installation_paused',
reason: 'Safety review in progress.',
});
});

it('returns the paused refusal for uninstall instead of a successful no-op', async () => {
installationService.uninstall.mockRejectedValue(
new installationService.InstallationPausedError('Safety review in progress.'),
);

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

expect(res.status).toBe(409);
expect(res.body).toMatchObject({
code: 'installation_paused',
reason: 'Safety review in progress.',
});
});
});
Loading
Loading