Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { TestingModule } from '@nestjs/testing'
import type { Prisma } from '@prisma/client'
import type { DeepMockProxy } from 'vitest-mock-extended'
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { faker } from '@faker-js/faker'
import { NotFoundException } from '@nestjs/common'
import { Test } from '@nestjs/testing'
Expand All @@ -17,6 +20,71 @@
} from '../project/project-testing.utils'
import { ProjectMembersService } from './project-members.service'

// --- Migration parity: projectMember event consumer surface --------------------
// Legacy apps/server fires `hook.projectMember.upsert` (add/patch) and
// `hook.projectMember.delete` (remove) in resources/project-member/business.ts.
// The NestJS rewrite emits the equivalent `projectMember.upsert` / `projectMember.delete`
// events from ProjectMembersService (add/patch/remove) but has NO
// `@OnEvent('projectMember.*')` consumer — the events are emitted and dropped.
// See apps/server-nestjs/documentation/MIGRATION-PARITY-MATRIX.md (the
// projectMember.* row and the "Redundant emission" note). This guard pins that
// gap so a half-migration (a listener for one verb but not the other, or a
// listener added without intent) is caught and forces a deliberate decision.
//
// The emit side is already locked by the add/patch/remove tests below
// (appEvents.emitProjectMemberEvent is called with projectMember.upsert /
// projectMember.delete at the correct call sites).

const PROJECT_MEMBER_EVENT_RE = /@OnEvent\(\s*['"](projectMember\.(upsert|delete))['"]/
const ANY_ON_EVENT_RE = /@OnEvent\(\s*['"]([^'"]+)['"]/

function collectSourceFiles(dir: string): string[] {
const out: string[] = []
for (const entry of readdirSync(dir)) {
if (entry === 'node_modules' || entry === 'dist') continue
const full = resolve(dir, entry)
if (statSync(full).isDirectory()) {
out.push(...collectSourceFiles(full))
} else if (entry.endsWith('.ts') && !entry.endsWith('.spec.ts') && !entry.endsWith('.e2e-spec.ts')) {
out.push(full)
}
}
return out
}

function readEventConsumers(re: RegExp): string[] {
const srcRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../')
const found: string[] = []
for (const file of collectSourceFiles(srcRoot)) {
const m = readFileSync(file, 'utf8').match(re)

Check notice on line 59 in apps/server-nestjs/src/modules/project-members/project-members.service.spec.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/project-members/project-members.service.spec.ts#L59

Use the "RegExp.exec()" method instead.
if (m) found.push(m[1])
}
return found
}

describe('migration parity: projectMember consumer', () => {
const projectMemberConsumers = readEventConsumers(PROJECT_MEMBER_EVENT_RE)
const allConsumers = readEventConsumers(ANY_ON_EVENT_RE)

it('scanner detects real @OnEvent consumers (guard against a vacuous negative test)', () => {
// Sanity: the walker must find live consumers (e.g. project.upsert) so the
// absence of projectMember.* consumers below is meaningful, not a broken scan.
expect(allConsumers).toContain('project.upsert')
})

it('documents the gap: no @OnEvent(\'projectMember.upsert\') consumer', () => {
// Legacy hook.projectMember.upsert fires on add/patch; nestjs emits
// projectMember.upsert but nothing listens. See MIGRATION-PARITY-MATRIX.md.
expect(projectMemberConsumers).not.toContain('projectMember.upsert')
})

it('documents the gap: no @OnEvent(\'projectMember.delete\') consumer', () => {
// Legacy hook.projectMember.delete fires on remove; nestjs emits
// projectMember.delete but nothing listens. See MIGRATION-PARITY-MATRIX.md.
expect(projectMemberConsumers).not.toContain('projectMember.delete')
})
})

describe('projectMembersService', () => {
let module: TestingModule
let service: ProjectMembersService
Expand Down
Loading