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
44 changes: 44 additions & 0 deletions apps/server-nestjs/src/modules/events/app-events.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'
import { Test } from '@nestjs/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mockDeep } from 'vitest-mock-extended'
import { ConfigurationService } from '../infrastructure/configuration/configuration.service'
import { PrismaService } from '../infrastructure/database/prisma.service'
import { LogService } from '../log/log.service'
import { projectSelect } from '../project/project-queries.utils'
Expand All @@ -17,18 +18,22 @@ describe('appEventsService', () => {
let prisma: DeepMockProxy<PrismaService>
let eventEmitter: DeepMockProxy<EventEmitter2Type>
let logs: DeepMockProxy<LogService>
let config: DeepMockProxy<ConfigurationService>

beforeEach(async () => {
prisma = mockDeep<PrismaService>()
eventEmitter = mockDeep<EventEmitter2>({ emitAsync: vi.fn().mockResolvedValue([]) })
logs = mockDeep<LogService>()
config = mockDeep<ConfigurationService>()
config.appVersion = 'test-version'

module = await Test.createTestingModule({
providers: [
AppEventsService,
{ provide: PrismaService, useValue: prisma },
{ provide: EventEmitter2, useValue: eventEmitter },
{ provide: LogService, useValue: logs },
{ provide: ConfigurationService, useValue: config },
],
}).compile()

Expand Down Expand Up @@ -88,6 +93,45 @@ describe('appEventsService', () => {
})
})

it('marks the project as failed when a listener reports a KO result', async () => {
const project = makeProject()
eventEmitter.emitAsync.mockResolvedValue([
{ argocd: { status: 'KO', message: 'Sync failed', executionTime: 20, error: new Error('Sync failed') } },
])

await service.emitProjectEvent('project.upsert', project, { action: 'Update Project' })

expect(prisma.project.update).toHaveBeenCalledWith({
where: { id: project.id },
data: { status: 'failed' },
})
})

it('marks the project as created with the provisioning version after a successful upsert', async () => {
const project = makeProject()
eventEmitter.emitAsync.mockResolvedValue([
{ gitlab: { status: 'OK', message: 'Up to date', executionTime: 10 } },
])

await service.emitProjectEvent('project.upsert', project, { action: 'Create Project' })

expect(prisma.project.update).toHaveBeenCalledWith({
where: { id: project.id },
data: { status: 'created', lastSuccessProvisionningVersion: 'test-version' },
})
})

it('does not touch the project status after a successful delete', async () => {
const project = makeProject()
eventEmitter.emitAsync.mockResolvedValue([
{ gitlab: { status: 'OK', message: 'Up to date', executionTime: 10 } },
])

await service.emitProjectEvent('project.delete', project, { action: 'Delete all project resources' })

expect(prisma.project.update).not.toHaveBeenCalled()
})

it('uses a provided project snapshot without fetching it again', async () => {
const project = makeProject()

Expand Down
35 changes: 33 additions & 2 deletions apps/server-nestjs/src/modules/events/app-events.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { PluginResults } from '../plugin/plugin.utils'
import type { ProjectWithDetails } from '../project/project-queries.utils'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { EventEmitter2 } from '@nestjs/event-emitter'
import { ConfigurationService } from '../infrastructure/configuration/configuration.service'
import { PrismaService } from '../infrastructure/database/prisma.service'
import { LogService } from '../log/log.service'
import { mergePluginResults } from '../plugin/plugin.utils'
import { getFailedPlugins, mergePluginResults } from '../plugin/plugin.utils'
import { getProject } from '../project/project-queries.utils'
import { formatEventLogData, isPluginResults } from './app-events.utils'

Expand Down Expand Up @@ -44,6 +45,7 @@ export class AppEventsService {
@Inject(PrismaService) private readonly prisma: PrismaService,
@Inject(EventEmitter2) private readonly eventEmitter: EventEmitter2,
@Inject(LogService) private readonly logs: LogService,
@Inject(ConfigurationService) private readonly config: ConfigurationService,
) {}

/**
Expand All @@ -68,7 +70,9 @@ export class AppEventsService {
return {}
}

return this.emitAndLog(event, project, project.id, context)
const results = await this.emitAndLog(event, project, project.id, context)
await this.updateProjectStatus(event, project.id, results)
return results
}

async emitProjectMemberEvent(
Expand Down Expand Up @@ -102,4 +106,31 @@ export class AppEventsService {

return results
}

/**
* Reflects the listeners' outcome on the project row (legacy hooks behavior):
* any KO result marks the project `failed`; a fully successful upsert marks it
* `created` and records the provisioning version. A successful `project.delete`
* leaves the `archived` status set when the project was archived.
Comment thread
KepoParis marked this conversation as resolved.
*/
private async updateProjectStatus(
event: ProjectEventName,
projectId: string,
results: PluginResults,
): Promise<void> {
const failed = getFailedPlugins(results)

if (failed.length) {
this.logger.warn(`${event} marked project as failed (projectId=${projectId}, failed=${failed.join(',')})`)
await this.prisma.project.update({ where: { id: projectId }, data: { status: 'failed' } })
return
}

if (event === 'project.upsert') {
await this.prisma.project.update({
where: { id: projectId },
data: { status: 'created', lastSuccessProvisionningVersion: this.config.appVersion },
})
}
}
}
27 changes: 21 additions & 6 deletions apps/server-nestjs/src/modules/events/app-events.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PluginResults } from '../plugin/plugin.utils'
import { NetworkError } from '@keycloak/keycloak-admin-client'
import { describe, expect, it } from 'vitest'
import { formatEventLogData, isPluginResults, serializeError } from './app-events.utils'

Expand All @@ -24,6 +25,22 @@ describe('serializeError', () => {
expect(parsed).toEqual({ name: 'Error', message: 'boom', stack: expect.any(String) })
})

it('should include HTTP details from errors carrying a fetch Response', () => {
const error = new NetworkError('Network response was not OK.', {
response: new Response('conflict', { status: 409 }),
responseData: { errorMessage: 'Sibling group named \'admin\' already exists.' },
})
const parsed = JSON.parse(serializeError(error))
expect(parsed).toEqual({
name: 'Error',
message: 'Network response was not OK.',
status: 409,
url: expect.any(String),
responseData: { errorMessage: 'Sibling group named \'admin\' already exists.' },
stack: expect.any(String),
})
})

it('should serialize non-Error values', () => {
expect(serializeError({ code: 42 })).toBe('{"code":42}')
expect(serializeError('boom')).toBe('"boom"')
Expand Down Expand Up @@ -58,21 +75,19 @@ describe('formatEventLogData', () => {
})
})

it('should produce an empty failed list and a success resume when everything is OK', () => {
it('should omit the failed list and produce a success resume when everything is OK', () => {
const results: PluginResults = {
gitlab: { status: 'OK', message: 'Up to date', executionTime: 10 },
}

expect(formatEventLogData(args, results, 10)).toEqual(expect.objectContaining({
failed: [],
messageResume: 'Success',
}))
const data = formatEventLogData(args, results, 10)
expect(data).not.toHaveProperty('failed')
expect(data.messageResume).toBe('Success')
})

it('should handle events without any listener', () => {
expect(formatEventLogData(args, {}, 1)).toEqual({
args,
failed: [],
results: {},
totalExecutionTime: 1,
messageResume: 'Success',
Expand Down
2 changes: 1 addition & 1 deletion apps/server-nestjs/src/modules/events/app-events.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function formatEventLogData(args: unknown, results: PluginResults, totalE

return {
args,
failed,
...(failed.length ? { failed } : {}),
Comment thread
KepoParis marked this conversation as resolved.
results: Object.fromEntries(entries.map(([service, result]) => [service, toLoggableResult(result)])),
totalExecutionTime: Math.round(totalExecutionTime),
messageResume: buildMessageResume(results, failed),
Expand Down