mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
refactor(audit): audit log writes through AuditLogRepository
This commit is contained in:
@@ -1,4 +1,65 @@
|
||||
import type { AuditLog } from '../entities/AuditLog.entity';
|
||||
import { type AssertRowKeys } from './_shared/rows';
|
||||
import { EntityRepository } from '@mikro-orm/sql';
|
||||
|
||||
export class AuditLogRepository extends EntityRepository<AuditLog> {}
|
||||
/**
|
||||
* An `audit_log` row as the API emits it (Plan 3i's `admin.service.ts`
|
||||
* projection reads it via a `LEFT JOIN users`, not this shape — this is the
|
||||
* bare table row).
|
||||
*/
|
||||
export interface AuditLogRow {
|
||||
id: number;
|
||||
created_at: string | null;
|
||||
user_id: number | null;
|
||||
action: string;
|
||||
resource: string | null;
|
||||
details: string | null;
|
||||
ip: string | null;
|
||||
}
|
||||
|
||||
const _auditLogRowKeys: AssertRowKeys<AuditLogRow, AuditLog> = true;
|
||||
|
||||
/** The column set `insert` writes; `id`/`created_at` are generated. */
|
||||
export interface NewAuditLogRow {
|
||||
user_id: number | null;
|
||||
action: string;
|
||||
resource: string | null;
|
||||
details: string | null;
|
||||
ip: string | null;
|
||||
}
|
||||
|
||||
// Reserved for Plan 3i (`admin.service.ts`'s paginated read of the same
|
||||
// table): `listPage(limit, offset): Promise<AuditLogPageRow[]>` (a `qb()`
|
||||
// join to `Users` for `username`/`email`) and `count(): Promise<number>`.
|
||||
// Not implemented here — see plan3a-sql-inventory.md §1.
|
||||
|
||||
export class AuditLogRepository extends EntityRepository<AuditLog> {
|
||||
/**
|
||||
* The column set of the legacy INSERT:
|
||||
* `INSERT INTO audit_log (user_id, action, resource, details, ip)
|
||||
* VALUES (?, ?, ?, ?, ?)`.
|
||||
*
|
||||
* Best-effort, fire-and-forget, exactly like the caller
|
||||
* (`AuditService.writeAudit`, wrapped in an outer try/catch that never
|
||||
* throws): no `refresh`, nothing read back — the generated `id` and
|
||||
* `created_at` (left to the column's `DEFAULT CURRENT_TIMESTAMP`) are
|
||||
* never used afterward.
|
||||
*
|
||||
* Named `insertEntry`, not `insert`: `EntityRepository` (`@mikro-orm/core`)
|
||||
* already declares a native `insert(data, options?): Promise<Primary<Entity>>`
|
||||
* (bypassing `create`/`persist`/`flush`), which is a `tsc`-verified
|
||||
* incompatible override for a `Promise<void>` method of the same name — the
|
||||
* same reason `Days.repository.ts`/`DayNotes.repository.ts` name their
|
||||
* inserts `createDay`/`createNote` rather than `create`.
|
||||
*/
|
||||
async insertEntry(entry: NewAuditLogRow): Promise<void> {
|
||||
const row = this.create({
|
||||
user: entry.user_id,
|
||||
action: entry.action,
|
||||
resource: entry.resource,
|
||||
details: entry.details,
|
||||
ip: entry.ip,
|
||||
});
|
||||
await this.getEntityManager().persist(row).flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { AuditLog } from '../../db/entities/AuditLog.entity';
|
||||
import { Users } from '../../db/entities/Users.entity';
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
/** Cross-cutting audit domain (Wave 2). No controller/MCP surface of its own —
|
||||
@@ -9,6 +12,7 @@ import { AuditService } from './audit.service';
|
||||
* transitively through each consumer's explicit import. Registered in
|
||||
* AppModule. */
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([AuditLog, Users])],
|
||||
providers: [AuditService],
|
||||
exports: [AuditService],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { AuditLog } from '../../db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../db/repositories/Users.repository';
|
||||
import { logInfo, logDebug, logError } from './audit-log.logger';
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
@@ -66,13 +70,16 @@ function buildInfoSummary(action: string, details?: Record<string, unknown>): st
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
constructor(private readonly dbs: DatabaseService) {}
|
||||
constructor(
|
||||
@InjectRepository(AuditLog) private readonly auditLog: AuditLogRepository,
|
||||
@InjectRepository(Users) private readonly users: UsersRepository,
|
||||
) {}
|
||||
|
||||
private async resolveUserEmail(userId: number | null): Promise<string> {
|
||||
if (userId == null) return 'anonymous';
|
||||
try {
|
||||
const row = this.dbs.get<{ email: string }>('SELECT email FROM users WHERE id = ?', userId);
|
||||
return row?.email || `uid:${userId}`;
|
||||
const email = await this.users.getEmail(userId);
|
||||
return email || `uid:${userId}`;
|
||||
} catch { return `uid:${userId}`; }
|
||||
}
|
||||
|
||||
@@ -87,10 +94,13 @@ export class AuditService {
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const detailsJson = entry.details && Object.keys(entry.details).length > 0 ? JSON.stringify(entry.details) : null;
|
||||
this.dbs.run(
|
||||
`INSERT INTO audit_log (user_id, action, resource, details, ip) VALUES (?, ?, ?, ?, ?)`,
|
||||
entry.userId, entry.action, entry.resource ?? null, detailsJson, entry.ip ?? null
|
||||
);
|
||||
await this.auditLog.insertEntry({
|
||||
user_id: entry.userId,
|
||||
action: entry.action,
|
||||
resource: entry.resource ?? null,
|
||||
details: detailsJson,
|
||||
ip: entry.ip ?? null,
|
||||
});
|
||||
|
||||
const email = await this.resolveUserEmail(entry.userId);
|
||||
const label = ACTION_LABELS[entry.action] || entry.action;
|
||||
|
||||
@@ -42,6 +42,7 @@ import { DatabaseModule } from '../../src/nest/database/database.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Atlas e2e (real auth guard + real service + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
@@ -49,7 +50,7 @@ describe('Atlas e2e (real auth guard + real service + temp SQLite)', () => {
|
||||
let userId: number;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, AtlasModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, AtlasModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -57,6 +57,7 @@ import { DatabaseModule } from '../../src/nest/database/database.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Auth e2e (real auth guard + real service + real cookie service + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
@@ -66,7 +67,7 @@ describe('Auth e2e (real auth guard + real service + real cookie service + temp
|
||||
let userPassword: string;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, AuthModule] })
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, AuthModule] })
|
||||
// The mailer is a provider since the notifications fold; overriding it is
|
||||
// the DI-native replacement for the old services/notifications module mock.
|
||||
.overrideProvider(MailerService)
|
||||
|
||||
@@ -48,13 +48,14 @@ import { BackupModule } from '../../src/nest/backup/backup.module';
|
||||
import { DatabaseModule } from '../../src/nest/database/database.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Backup e2e (real auth + admin guard + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, BackupModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, BackupModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -77,13 +77,14 @@ import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { RateLimitService } from '../../src/nest/common/rate-limit.service';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Collab e2e (real auth guard + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, RealtimeModule, CollabModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, RealtimeModule, CollabModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -46,6 +46,7 @@ import { AddonsService } from '../../src/nest/addons/addons.service';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Collections e2e (real auth guard + real service + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
@@ -55,7 +56,7 @@ describe('Collections e2e (real auth guard + real service + temp SQLite)', () =>
|
||||
let tripId: number;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, RealtimeModule, CollectionsModule] })
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, RealtimeModule, CollectionsModule] })
|
||||
.overrideProvider(AddonsService)
|
||||
.useValue({ isAddonEnabled })
|
||||
.compile();
|
||||
|
||||
@@ -84,6 +84,7 @@ import { NextcloudDocumentProvider, OpencloudDocumentProvider } from '../../src/
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
/** A provider that answers plausibly but never opens a socket. */
|
||||
function fakeProvider(id: string) {
|
||||
@@ -137,7 +138,7 @@ describe('Document sync e2e (real guards + real services + temp SQLite)', () =>
|
||||
[OpencloudDocumentProvider, 'opencloud'],
|
||||
[SynologyDriveDocumentProvider, 'synologydrive'],
|
||||
] as const;
|
||||
let builder = Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, DocSyncModule] })
|
||||
let builder = Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, DocSyncModule] })
|
||||
.overrideProvider(AddonsService)
|
||||
.useValue({ isAddonEnabled });
|
||||
const fakes = providers.map(([, id]) => fakeProvider(id));
|
||||
|
||||
@@ -82,13 +82,14 @@ import { FilesModule } from '../../src/nest/files/files.module';
|
||||
import { PhotosModule } from '../../src/nest/photos/photos.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Files + photos e2e (real auth guard + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, RealtimeModule, FilesModule, PhotosModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, RealtimeModule, FilesModule, PhotosModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -31,13 +31,14 @@ import { DatabaseModule } from '../../src/nest/database/database.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Maps e2e (real auth guard + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, MapsModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, MapsModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -68,6 +68,7 @@ import { MemoriesAccessService } from '../../src/nest/memories/memories-access.s
|
||||
import { RealtimeModule } from '../../src/nest/realtime/realtime.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
const BASE = '/api/integrations/memories';
|
||||
const UNIFIED = `${BASE}/unified`;
|
||||
@@ -79,7 +80,7 @@ describe('Memories e2e (real auth guard + temp SQLite)', () => {
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, RealtimeModule, MemoriesModule] })
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, RealtimeModule, MemoriesModule] })
|
||||
.overrideProvider(UnifiedMemoriesService).useValue(unified)
|
||||
.overrideProvider(ImmichService).useValue(immich)
|
||||
.overrideProvider(SynologyService).useValue(synology)
|
||||
|
||||
@@ -71,6 +71,7 @@ import { NtfyService } from '../../src/nest/notifications/transports/ntfy.servic
|
||||
import { WebhookService } from '../../src/nest/notifications/transports/webhook.service';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
function seedNotification(recipientId: number, overrides: { is_read?: number } = {}): number {
|
||||
const r = db.prepare(
|
||||
@@ -86,7 +87,7 @@ describe('Notifications e2e (real auth guard + temp SQLite)', () => {
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, RealtimeModule, NotificationsModule],
|
||||
imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, RealtimeModule, NotificationsModule],
|
||||
})
|
||||
.overrideProvider(NotificationPreferencesService).useValue(prefs)
|
||||
.overrideProvider(MailerService).useValue(mailer)
|
||||
|
||||
@@ -57,13 +57,14 @@ import { AddonsService } from '../../src/nest/addons/addons.service';
|
||||
import { DatabaseModule } from '../../src/nest/database/database.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('OAuth e2e (real guards + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, OauthModule] })
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, OauthModule] })
|
||||
.overrideProvider(AddonsService)
|
||||
.useValue({ isAddonEnabled })
|
||||
.overrideProvider(OauthService)
|
||||
|
||||
@@ -51,6 +51,7 @@ import { DatabaseModule } from '../../src/nest/database/database.module';
|
||||
import { AuthService } from '../../src/nest/auth/auth.service';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('OIDC e2e (real cookie service)', () => {
|
||||
let server: Server;
|
||||
@@ -58,7 +59,7 @@ describe('OIDC e2e (real cookie service)', () => {
|
||||
let consumeAuthCode: MockInstance;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, OidcModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, OidcModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -38,6 +38,7 @@ import { RateLimitService } from '../../src/nest/common/rate-limit.service';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
const BODY = { lat: 50.9, lng: 6.96, name: 'Museum Ludwig', placeId: 'way:12345' };
|
||||
|
||||
@@ -47,7 +48,7 @@ describe('Place enrichment e2e (real auth guard + real validation pipe)', () =>
|
||||
let maps: MapsService;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, PlaceEnrichmentModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, PlaceEnrichmentModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -58,6 +58,7 @@ vi.mock('../../src/config', () => ({ ENCRYPTION_KEY: 'e2e-storage-key', JWT_SECR
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
@@ -67,7 +68,7 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, StorageModule],
|
||||
imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, StorageModule],
|
||||
providers: [{ provide: APP_GUARD, useClass: ManagedGuard }],
|
||||
}).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
|
||||
@@ -38,6 +38,7 @@ import { DatabaseModule } from '../../src/nest/database/database.module';
|
||||
import { TagsModule } from '../../src/nest/tags/tags.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
function insertTag(userId: number, name: string, color = '#10b981'): number {
|
||||
const res = db.prepare('INSERT INTO tags (user_id, name, color) VALUES (?, ?, ?)').run(userId, name, color);
|
||||
@@ -49,7 +50,7 @@ describe('Tags e2e (real auth guard + temp SQLite)', () => {
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, TagsModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, TagsModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -80,13 +80,14 @@ import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { broadcastToUser } from '../../src/websocket';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
|
||||
describe('Vacay e2e (real auth guard + temp SQLite)', () => {
|
||||
let server: Server;
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), DatabaseModule, RealtimeModule, VacayModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), DatabaseModule, RealtimeModule, VacayModule] }).compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
|
||||
@@ -40,9 +40,14 @@ import { OauthService } from '../../src/nest/oauth/oauth.service';
|
||||
import { DatabaseService } from '../../src/nest/database/database.service';
|
||||
import { AddonsService } from '../../src/nest/addons/addons.service';
|
||||
import { AuditService } from '../../src/nest/audit/audit.service';
|
||||
import { createTestOrm } from '../helpers/test-orm';
|
||||
import { AuditLog } from '../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../src/db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../src/db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../src/db/repositories/Users.repository';
|
||||
|
||||
const oauthDbs = new DatabaseService(testDb);
|
||||
const oauthSvc = new OauthService(oauthDbs, new AddonsService(oauthDbs), new AuditService(oauthDbs));
|
||||
let oauthSvc: OauthService;
|
||||
|
||||
/** Mint a trekoa_ access token for the user via a fresh OAuth client. */
|
||||
async function mintOauthToken(userId: number, audience: string | null, scopes: string[] = ['trips:read']): Promise<{ accessToken: string; clientId: string }> {
|
||||
@@ -69,6 +74,8 @@ let app: Application;
|
||||
beforeAll(async () => {
|
||||
nestApp = await buildApp();
|
||||
app = nestApp.getHttpAdapter().getInstance();
|
||||
const t = await createTestOrm(testDb);
|
||||
oauthSvc = new OauthService(oauthDbs, new AddonsService(oauthDbs), new AuditService(t.repo(AuditLog) as AuditLogRepository, t.repo(Users) as UsersRepository));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -44,13 +44,18 @@ import { OauthService } from '../../src/nest/oauth/oauth.service';
|
||||
import { DatabaseService } from '../../src/nest/database/database.service';
|
||||
import { AddonsService } from '../../src/nest/addons/addons.service';
|
||||
import { AuditService } from '../../src/nest/audit/audit.service';
|
||||
import { createTestOrm } from '../helpers/test-orm';
|
||||
import { AuditLog } from '../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../src/db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../src/db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../src/db/repositories/Users.repository';
|
||||
|
||||
// The consent controller writes pending codes through the container instance;
|
||||
// the SDK-mounted authorize path reads them back. The map is module-scoped in
|
||||
// oauth.pending-codes.ts, so a hand-built service instance shares it — the
|
||||
// same single-instance property the full auth-code loop below proves end-to-end.
|
||||
const oauthDbs = new DatabaseService(testDb);
|
||||
const containerSideOauth = new OauthService(oauthDbs, new AddonsService(oauthDbs), new AuditService(oauthDbs));
|
||||
let containerSideOauth: OauthService;
|
||||
|
||||
let nestApp: INestApplication;
|
||||
let app: Application;
|
||||
@@ -80,6 +85,8 @@ async function registerClient(redirectUri = 'https://client.example.com/cb', sco
|
||||
beforeAll(async () => {
|
||||
nestApp = await buildApp();
|
||||
app = nestApp.getHttpAdapter().getInstance();
|
||||
const t = await createTestOrm(testDb);
|
||||
containerSideOauth = new OauthService(oauthDbs, new AddonsService(oauthDbs), new AuditService(t.repo(AuditLog) as AuditLogRepository, t.repo(Users) as UsersRepository));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -41,6 +41,11 @@ import { OauthService } from '../../src/nest/oauth/oauth.service';
|
||||
import { DatabaseService } from '../../src/nest/database/database.service';
|
||||
import { AddonsService } from '../../src/nest/addons/addons.service';
|
||||
import { AuditService } from '../../src/nest/audit/audit.service';
|
||||
import { createTestOrm } from '../helpers/test-orm';
|
||||
import { AuditLog } from '../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../src/db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../src/db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../src/db/repositories/Users.repository';
|
||||
|
||||
// In production the consent controller writes pending codes through the
|
||||
// container instance and the SDK routes read them back through the same
|
||||
@@ -48,7 +53,7 @@ import { AuditService } from '../../src/nest/audit/audit.service';
|
||||
// does: through a service instance. The pending-code map is module-scoped in
|
||||
// oauth.pending-codes.ts, so the routes under test see every code written here.
|
||||
const oauthDbs = new DatabaseService(testDb);
|
||||
const containerSideOauth = new OauthService(oauthDbs, new AddonsService(oauthDbs), new AuditService(oauthDbs));
|
||||
let containerSideOauth: OauthService;
|
||||
// Arrow wrappers rather than `.bind`: with `strictBindCallApply: false` a bound
|
||||
// alias is typed `any`, which hides a missing `await` on the now-async methods.
|
||||
const createAuthCode = (...args: Parameters<OauthService['createAuthCode']>) => containerSideOauth.createAuthCode(...args);
|
||||
@@ -78,6 +83,8 @@ function setMcpEnabled(enabled: boolean) {
|
||||
beforeAll(async () => {
|
||||
nestApp = await buildApp();
|
||||
app = nestApp.getHttpAdapter().getInstance();
|
||||
const t = await createTestOrm(testDb);
|
||||
containerSideOauth = new OauthService(oauthDbs, new AddonsService(oauthDbs), new AuditService(t.repo(AuditLog) as AuditLogRepository, t.repo(Users) as UsersRepository));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -59,6 +59,11 @@ import { PluginRpcHostFactory } from '../../../src/nest/plugins/host/plugin-rpc-
|
||||
import { PluginRpcRegistry } from '../../../src/nest/plugins/host/rpc-kit/registry';
|
||||
import type { PluginRpcRegistryService } from '../../../src/nest/plugins/host/rpc-kit/registry.service';
|
||||
import { DbRpc } from '../../../src/nest/plugins/host/rpc/db.rpc';
|
||||
import { createTestOrm } from '../../helpers/test-orm';
|
||||
import { AuditLog } from '../../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../../src/db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../../src/db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
|
||||
|
||||
let codeRoot: string;
|
||||
let dataRoot: string;
|
||||
@@ -104,6 +109,9 @@ describe('plugin boot vs registry scan ordering', () => {
|
||||
// own onModuleInit scan has run.
|
||||
const registry = new PluginRpcRegistry();
|
||||
const hostFactory = new PluginRpcHostFactory(dbs, registry as unknown as PluginRpcRegistryService);
|
||||
const t = await createTestOrm(dbConn);
|
||||
const auditLogRepo = t.repo(AuditLog) as AuditLogRepository;
|
||||
const usersRepo = t.repo(Users) as UsersRepository;
|
||||
|
||||
mod = await Test.createTestingModule({
|
||||
providers: [
|
||||
@@ -112,7 +120,7 @@ describe('plugin boot vs registry scan ordering', () => {
|
||||
// (if it had one) first. The boot reconcile must therefore not live there.
|
||||
{
|
||||
provide: PluginRuntimeService,
|
||||
useFactory: () => new PluginRuntimeService(dbs, new AuditService(dbs), new AddonsService(dbs), userSettings, undefined, hostFactory),
|
||||
useFactory: () => new PluginRuntimeService(dbs, new AuditService(auditLogRepo, usersRepo), new AddonsService(dbs), userSettings, undefined, hostFactory),
|
||||
},
|
||||
{
|
||||
provide: 'REGISTRY_SCAN',
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createSnapshotTestDb } from '../../../helpers/db-mock';
|
||||
import { resetTestDb } from '../../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
||||
import { createUser } from '../../../helpers/factories';
|
||||
import { AuditLog } from '../../../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../../../src/db/repositories/AuditLog.repository';
|
||||
import { DB_TIMESTAMP_RE } from '../../../../src/db/types';
|
||||
|
||||
const testDb = createSnapshotTestDb();
|
||||
let t: TestOrm;
|
||||
let auditLog: AuditLogRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await createTestOrm(testDb);
|
||||
auditLog = t.repo(AuditLog) as AuditLogRepository;
|
||||
});
|
||||
beforeEach(() => { resetTestDb(testDb); t.clear(); });
|
||||
afterAll(async () => { await t.close(); testDb.close(); });
|
||||
|
||||
describe('AuditLogRepository', () => {
|
||||
it('AUDITREPO-001: insert writes the legacy column set, matching the SELECT * row exactly', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await auditLog.insertEntry({ user_id: user.id, action: 'trip.create', resource: 'trip', details: '{"title":"Rome"}', ip: '1.2.3.4' });
|
||||
const row = testDb.prepare('SELECT * FROM audit_log').get() as { id: number; created_at: string };
|
||||
expect(row).toStrictEqual({
|
||||
id: row.id,
|
||||
created_at: row.created_at,
|
||||
user_id: user.id,
|
||||
action: 'trip.create',
|
||||
resource: 'trip',
|
||||
details: '{"title":"Rome"}',
|
||||
ip: '1.2.3.4',
|
||||
});
|
||||
});
|
||||
|
||||
it('AUDITREPO-002: created_at is left to the column default, as text, in the YYYY-MM-DD HH:MM:SS shape', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await auditLog.insertEntry({ user_id: user.id, action: 'user.login', resource: null, details: null, ip: null });
|
||||
const stored = testDb.prepare('SELECT created_at, typeof(created_at) AS kind FROM audit_log').get() as { created_at: string; kind: string };
|
||||
expect(stored.kind).toBe('text');
|
||||
expect(stored.created_at).toMatch(DB_TIMESTAMP_RE);
|
||||
});
|
||||
|
||||
it('AUDITREPO-003: a null user writes a null user_id twin, not a foreign-key value', async () => {
|
||||
await auditLog.insertEntry({ user_id: null, action: 'user.login', resource: null, details: null, ip: null });
|
||||
const row = testDb.prepare('SELECT user_id FROM audit_log').get() as { user_id: number | null };
|
||||
expect(row.user_id).toBeNull();
|
||||
});
|
||||
|
||||
it('AUDITREPO-004: null resource/details/ip are stored as NULL, not the string "null"', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await auditLog.insertEntry({ user_id: user.id, action: 'user.login', resource: null, details: null, ip: null });
|
||||
const row = testDb.prepare('SELECT resource, details, ip FROM audit_log').get();
|
||||
expect(row).toEqual({ resource: null, details: null, ip: null });
|
||||
});
|
||||
|
||||
it('AUDITREPO-005: resolves without returning a value — no id or row is read back', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await expect(
|
||||
auditLog.insertEntry({ user_id: user.id, action: 'user.login', resource: null, details: null, ip: null }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,32 +1,15 @@
|
||||
/**
|
||||
* Unit tests for the DI-native audit domain — AUDIT-SVC-001 through
|
||||
* AUDIT-SVC-017 (001–007 are the getClientIp cases moved 1:1 from the legacy
|
||||
* AUDIT-SVC-019 (001–007 are the getClientIp cases moved 1:1 from the legacy
|
||||
* tests/unit/services/auditLog.test.ts, which had no case IDs — the IDs are
|
||||
* introduced with the move; 008–014 cover writeAudit over a real in-memory
|
||||
* SQLite DB, which the legacy suite never exercised; 015–017 pin the
|
||||
* audit.bridge delegation). The logger module is mocked (it replaces the old
|
||||
* suite's fs mock: the import-time mkdir lives there now, and mocking it lets
|
||||
* the exact log-line formats be asserted).
|
||||
* introduced with the move; 008–014, 018, 019 cover writeAudit over a real
|
||||
* in-memory SQLite DB through AuditLogRepository/UsersRepository, which the
|
||||
* legacy suite never exercised). The logger module is mocked (it replaces the
|
||||
* old suite's fs mock: the import-time mkdir lives there now, and mocking it
|
||||
* lets the exact log-line formats be asserted).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
|
||||
// ── DB setup ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const { testDb, dbMock } = vi.hoisted(() => {
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(':memory:');
|
||||
db.exec('PRAGMA journal_mode = WAL');
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
db.exec('PRAGMA busy_timeout = 5000');
|
||||
const mock = {
|
||||
db,
|
||||
closeDb: () => {},
|
||||
reinitialize: () => {},
|
||||
};
|
||||
return { testDb: db, dbMock: mock };
|
||||
});
|
||||
|
||||
vi.mock('../../../src/db/database', () => dbMock);
|
||||
vi.mock('../../../src/nest/audit/audit-log.logger', () => ({
|
||||
LOG_LEVEL: 'error',
|
||||
logInfo: vi.fn(),
|
||||
@@ -36,27 +19,44 @@ vi.mock('../../../src/nest/audit/audit-log.logger', () => ({
|
||||
}));
|
||||
|
||||
import type { Request } from 'express';
|
||||
import Database from 'better-sqlite3';
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { AuditLog } from '../../../src/db/entities/AuditLog.entity';
|
||||
import { Users } from '../../../src/db/entities/Users.entity';
|
||||
import type { AuditLogRepository } from '../../../src/db/repositories/AuditLog.repository';
|
||||
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
|
||||
import { AuditService } from '../../../src/nest/audit/audit.service';
|
||||
import { getClientIp } from '../../../src/nest/audit/client-ip';
|
||||
import { logInfo, logDebug, logError } from '../../../src/nest/audit/audit-log.logger';
|
||||
|
||||
const svc = new AuditService(new DatabaseService(testDb));
|
||||
const testDb = new Database(':memory:');
|
||||
testDb.exec('PRAGMA journal_mode = WAL');
|
||||
testDb.exec('PRAGMA foreign_keys = ON');
|
||||
testDb.exec('PRAGMA busy_timeout = 5000');
|
||||
|
||||
beforeAll(() => {
|
||||
let t: TestOrm;
|
||||
let auditLogRepo: AuditLogRepository;
|
||||
let svc: AuditService;
|
||||
|
||||
beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
t = await createTestOrm(testDb);
|
||||
auditLogRepo = t.repo(AuditLog) as AuditLogRepository;
|
||||
svc = new AuditService(auditLogRepo, t.repo(Users) as UsersRepository);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testDb.prepare('DELETE FROM audit_log').run();
|
||||
testDb.prepare('DELETE FROM users').run();
|
||||
t.clear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
await t.close();
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('getClientIp', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── writeAudit (real DB) ──────────────────────────────────────────────────────
|
||||
// ── writeAudit (real DB, through the repositories) ────────────────────────────
|
||||
|
||||
function seedUser(id: number, email: string): void {
|
||||
testDb.prepare(
|
||||
@@ -180,10 +180,10 @@ describe('writeAudit', () => {
|
||||
});
|
||||
|
||||
it('AUDIT-SVC-013: never throws — a failed insert reduces to a logError line', async () => {
|
||||
testDb.prepare('ALTER TABLE audit_log RENAME TO audit_log_gone').run();
|
||||
const spy = vi.spyOn(auditLogRepo, 'insertEntry').mockRejectedValueOnce(new Error('insert failed'));
|
||||
await expect(svc.writeAudit({ userId: 1, action: 'user.login' })).resolves.not.toThrow();
|
||||
expect(logError).toHaveBeenCalledWith(expect.stringMatching(/^Audit write failed: /));
|
||||
testDb.prepare('ALTER TABLE audit_log_gone RENAME TO audit_log').run();
|
||||
expect(logError).toHaveBeenCalledWith('Audit write failed: insert failed');
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('AUDIT-SVC-018: settings.api_keys_update names the changed keys and nothing else (#1939)', async () => {
|
||||
@@ -212,4 +212,3 @@ describe('writeAudit', () => {
|
||||
expect(logInfo).toHaveBeenLastCalledWith('a@b.c logged in ip=1.1.1.1');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -65,12 +65,19 @@ import type { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import { getMcpSafeUrl } from '../../../src/app-config';
|
||||
import { ADDON_IDS } from '../../../src/addons';
|
||||
import { MAX_PENDING_CODES, sweepPendingCodes } from '../../../src/nest/oauth/oauth.pending-codes';
|
||||
import { createTestOrm } from '../../helpers/test-orm';
|
||||
import { AuditLog } from '../../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../../src/db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../../src/db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
|
||||
|
||||
const dbs = new DatabaseService(testDb);
|
||||
// Stubbed rather than real: every case drives the MCP gate through this one
|
||||
// flag, exactly as the addons.bridge mock did before the fold.
|
||||
const addonsStub = { isAddonEnabled } as unknown as AddonsService;
|
||||
const svc = new OauthService(dbs, addonsStub, new AuditService(dbs));
|
||||
let svc: OauthService;
|
||||
let auditLogRepo: AuditLogRepository;
|
||||
let usersRepo: UsersRepository;
|
||||
|
||||
// Legacy free-function names delegating to the service, so the moved cases below
|
||||
// read exactly as they did before the fold. Arrow wrappers rather than `.bind`:
|
||||
@@ -95,9 +102,13 @@ const saveConsent = (...args: Parameters<OauthService['saveConsent']>) => svc.sa
|
||||
const getConsent = (...args: Parameters<OauthService['getConsent']>) => svc.getConsent(...args);
|
||||
const isConsentSufficient = (...args: Parameters<OauthService['isConsentSufficient']>) => svc.isConsentSufficient(...args);
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
const t = await createTestOrm(testDb);
|
||||
auditLogRepo = t.repo(AuditLog) as AuditLogRepository;
|
||||
usersRepo = t.repo(Users) as UsersRepository;
|
||||
svc = new OauthService(dbs, addonsStub, new AuditService(auditLogRepo, usersRepo));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1285,7 +1296,7 @@ describe('module-scoped OAuth state', () => {
|
||||
codeChallengeMethod: 'S256',
|
||||
})!;
|
||||
|
||||
const secondInstance = new OauthService(dbs, addonsStub, new AuditService(dbs));
|
||||
const secondInstance = new OauthService(dbs, addonsStub, new AuditService(auditLogRepo, usersRepo));
|
||||
expect(secondInstance.consumeAuthCode(code)?.userId).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,11 @@ import { runMigrations } from '../../../src/db/migrations';
|
||||
import { createUser } from '../../helpers/factories';
|
||||
import { PluginUserSettingsService } from '../../../src/nest/plugins/plugin-user-settings.service';
|
||||
import { parseManifest, ManifestError } from '../../../src/nest/plugins/install/manifest';
|
||||
import { createTestOrm } from '../../helpers/test-orm';
|
||||
import { AuditLog } from '../../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../../src/db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../../src/db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
|
||||
/** The host-side settings reads, over the same connection the test seeded. */
|
||||
const userSettings = () => new PluginUserSettingsService(new DatabaseService(dbConn));
|
||||
|
||||
@@ -126,7 +131,13 @@ describe('a plugin channel label is bounded by the host', () => {
|
||||
VALUES ('loud', 'Loud', 'active', 1, '1.0.0', '[]', '[]', ?, '{}')`,
|
||||
).run(JSON.stringify({ notificationChannel: { title: '🎉'.repeat(5) + 'A'.repeat(500) } }));
|
||||
|
||||
const rt = new PluginRuntimeService(new DatabaseService(dbConn), new AuditService(new DatabaseService(dbConn)), new AddonsService(new DatabaseService(dbConn)), userSettings());
|
||||
const t = await createTestOrm(dbConn);
|
||||
const rt = new PluginRuntimeService(
|
||||
new DatabaseService(dbConn),
|
||||
new AuditService(t.repo(AuditLog) as AuditLogRepository, t.repo(Users) as UsersRepository),
|
||||
new AddonsService(new DatabaseService(dbConn)),
|
||||
userSettings(),
|
||||
);
|
||||
// Stand the plugin up as a granted, active notificationChannel provider.
|
||||
(rt as unknown as { supervisor: { running: Map<string, unknown> } }).supervisor.running.set('loud', {
|
||||
id: 'loud', status: 'active', hooks: ['notificationChannel'], granted: new Set(['hook:notification-channel']),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
|
||||
|
||||
// Avoid any real DNS/network from the SSRF guard during saveSettings and the probe.
|
||||
vi.mock('../../../src/utils/ssrfGuard', () => ({
|
||||
@@ -12,15 +12,25 @@ import { AirtrailService } from '../../../src/nest/integrations/airtrail.service
|
||||
import type { AirtrailClient } from '../../../src/nest/integrations/airtrail.client';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { AuditService } from '../../../src/nest/audit/audit.service';
|
||||
import { createTestOrm } from '../../helpers/test-orm';
|
||||
import { AuditLog } from '../../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../../src/db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../../src/db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
|
||||
|
||||
// The probe is the only call that would leave the process, so the client is a
|
||||
// stub; the credential handling around it runs against the real row.
|
||||
const listFlights = vi.fn();
|
||||
const svc = new AirtrailService(
|
||||
new DatabaseService(db),
|
||||
new AuditService(new DatabaseService(db)),
|
||||
{ listFlights } as unknown as AirtrailClient,
|
||||
);
|
||||
let svc: AirtrailService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const t = await createTestOrm(db);
|
||||
svc = new AirtrailService(
|
||||
new DatabaseService(db),
|
||||
new AuditService(t.repo(AuditLog) as AuditLogRepository, t.repo(Users) as UsersRepository),
|
||||
{ listFlights } as unknown as AirtrailClient,
|
||||
);
|
||||
});
|
||||
|
||||
const MASK = '••••••••';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
||||
|
||||
// Avoid any real DNS/network from the SSRF guard during saveSettings.
|
||||
vi.mock('../../../src/utils/ssrfGuard', () => ({
|
||||
@@ -12,17 +12,30 @@ import { AirtrailService } from '../../../src/nest/integrations/airtrail.service
|
||||
import { AirtrailClient } from '../../../src/nest/integrations/airtrail.client';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { AuditService } from '../../../src/nest/database/../audit/audit.service';
|
||||
import { createTestOrm } from '../../helpers/test-orm';
|
||||
import { AuditLog } from '../../../src/db/entities/AuditLog.entity';
|
||||
import type { AuditLogRepository } from '../../../src/db/repositories/AuditLog.repository';
|
||||
import { Users } from '../../../src/db/entities/Users.entity';
|
||||
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
|
||||
|
||||
// The free functions became methods with the airtrail fold; same SQL, same
|
||||
// behaviour, one instance over the same db handle.
|
||||
const svc = new AirtrailService(
|
||||
new DatabaseService(db),
|
||||
new AuditService(new DatabaseService(db)),
|
||||
new AirtrailClient(),
|
||||
);
|
||||
const getConnectionSettings = (...args: Parameters<AirtrailService['getConnectionSettings']>) => svc.getConnectionSettings(...args);
|
||||
const isAirtrailWriteEnabled = (...args: Parameters<AirtrailService['isAirtrailWriteEnabled']>) => svc.isAirtrailWriteEnabled(...args);
|
||||
const saveSettings = (...args: Parameters<AirtrailService['saveSettings']>) => svc.saveSettings(...args);
|
||||
let svc: AirtrailService;
|
||||
let getConnectionSettings: (...args: Parameters<AirtrailService['getConnectionSettings']>) => ReturnType<AirtrailService['getConnectionSettings']>;
|
||||
let isAirtrailWriteEnabled: (...args: Parameters<AirtrailService['isAirtrailWriteEnabled']>) => ReturnType<AirtrailService['isAirtrailWriteEnabled']>;
|
||||
let saveSettings: (...args: Parameters<AirtrailService['saveSettings']>) => ReturnType<AirtrailService['saveSettings']>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const t = await createTestOrm(db);
|
||||
svc = new AirtrailService(
|
||||
new DatabaseService(db),
|
||||
new AuditService(t.repo(AuditLog) as AuditLogRepository, t.repo(Users) as UsersRepository),
|
||||
new AirtrailClient(),
|
||||
);
|
||||
getConnectionSettings = (...args) => svc.getConnectionSettings(...args);
|
||||
isAirtrailWriteEnabled = (...args) => svc.isAirtrailWriteEnabled(...args);
|
||||
saveSettings = (...args) => svc.saveSettings(...args);
|
||||
});
|
||||
|
||||
describe('airtrail writeback opt-in persistence (#1240)', () => {
|
||||
it('defaults the writeback opt-in to off for a new user', async () => {
|
||||
|
||||
Reference in New Issue
Block a user