Skip to content

Commit

Permalink
feat(api-radio-communication-system): introduce TETRA SDS, Call Out a…
Browse files Browse the repository at this point in the history
…nd Status updates
  • Loading branch information
timonmasberg committed Jan 24, 2024
1 parent 99a715d commit a12c4e9
Show file tree
Hide file tree
Showing 26 changed files with 939 additions and 26 deletions.
18 changes: 18 additions & 0 deletions libs/api/radio-communication-system/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
16 changes: 16 additions & 0 deletions libs/api/radio-communication-system/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Radio Communication System

This module covers the radio communication currently over tetra control. The
TetraControlService can send out Call Outs, SDS and Status changes. Also, a
webhook handler for incoming status changes is implemented. The webhook has to
be configured with a key via `TETRA_CONTROL_WEBHOOK_KEY`, which has to be
provided in Tetra Control
`<kordis-api-url>/webhooks/tetra-control?key=<safe-key>`. With
`TETRA_CONTROL_WEBHOOK_VALID_IPS`, a list of allowed IPs can be provided
(delimited by a comma). If the list is empty, all IPs are allowed. If a new SDS
is received, a `NewTetraStatusEvent` is emitted.

## Running unit tests

Run `nx test api-radio-communication-system` to execute the unit tests via
[Jest](https://jestjs.io).
11 changes: 11 additions & 0 deletions libs/api/radio-communication-system/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'api-radio-communication-system',
preset: '../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/api/radio-communication-system',
};
30 changes: 30 additions & 0 deletions libs/api/radio-communication-system/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "api-radio-communication-system",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/api/radio-communication-system/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/api/radio-communication-system/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/api/radio-communication-system/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": []
}
2 changes: 2 additions & 0 deletions libs/api/radio-communication-system/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/radio-communication-system.module';
export * from './lib/core/event/new-tetra-status.event';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createMock } from '@golevelup/ts-jest';
import { EventBus } from '@nestjs/cqrs';

import { NewTetraStatusEvent } from '../event/new-tetra-status.event';
import {
PublishTetraStatusCommand,
PublishTetraStatusHandler,
} from './publish-tetra-status.command';

describe('PublishTetraStatusHandler', () => {
let handler: PublishTetraStatusHandler;
let eventBus: EventBus;

beforeEach(() => {
eventBus = createMock<EventBus>();
handler = new PublishTetraStatusHandler(eventBus);
});

it('should publish NewRadioCallStatusReportEvent', async () => {
const sendingIssi = '12345';
const fmsStatus = 1;
const sentAt = new Date();

const command = new PublishTetraStatusCommand(
sendingIssi,
fmsStatus,
sentAt,
);
const publishSpy = jest.spyOn(eventBus, 'publish');
await handler.execute(command);

expect(publishSpy).toHaveBeenCalledWith(
new NewTetraStatusEvent(sendingIssi, fmsStatus, sentAt),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
CommandHandler,
EventBus,
ICommand,
ICommandHandler,
} from '@nestjs/cqrs';

import { NewTetraStatusEvent } from '../event/new-tetra-status.event';

export class PublishTetraStatusCommand implements ICommand {
constructor(
readonly sendingIssi: string,
readonly fmsStatus: number,
readonly sentAt: Date,
) {}
}

@CommandHandler(PublishTetraStatusCommand)
export class PublishTetraStatusHandler
implements ICommandHandler<PublishTetraStatusCommand>
{
constructor(private readonly eventBus: EventBus) {}

async execute(command: PublishTetraStatusCommand): Promise<void> {
this.eventBus.publish(
new NewTetraStatusEvent(
command.sendingIssi,
command.fmsStatus,
command.sentAt,
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { DeepMocked, createMock } from '@golevelup/ts-jest';

import { TetraControlService } from '../../infra/service/tetra-control.service';
import { SdsNotAbleToSendException } from '../exception/sds-not-able-to-send.exception';
import {
SendTetraSDSCommand,
SendTetraSDSHandler,
} from './send-tetra-sds.command';

describe('SendTetraSDSHandler', () => {
let handler: SendTetraSDSHandler;
let tetraServiceMock: DeepMocked<TetraControlService>;

beforeEach(() => {
tetraServiceMock = createMock<TetraControlService>();
handler = new SendTetraSDSHandler(tetraServiceMock);
});

it('should call send sds through tetra service', async () => {
const issi = '12345';
const message = 'Test message';
const isFlash = true;

const command = new SendTetraSDSCommand(issi, message, isFlash);

await handler.execute(command);

expect(tetraServiceMock.sendSDS).toHaveBeenCalledWith(
issi,
message,
isFlash,
);
});

it('should throw an error if tetraService.sendSDS throws an error', async () => {
const issi = '12345';
const message = 'Test message';
const isFlash = true;

const command = new SendTetraSDSCommand(issi, message, isFlash);

tetraServiceMock.sendSDS.mockRejectedValue(new Error());

await expect(handler.execute(command)).rejects.toThrow(
SdsNotAbleToSendException,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

import { SdsNotAbleToSendException } from '../exception/sds-not-able-to-send.exception';
import { TETRA_SERVICE, TetraService } from '../service/tetra.service';

export class SendTetraSDSCommand {
constructor(
readonly issi: string,
readonly message: string,
readonly isFlash: boolean,
) {}
}

@CommandHandler(SendTetraSDSCommand)
export class SendTetraSDSHandler
implements ICommandHandler<SendTetraSDSCommand>
{
constructor(
@Inject(TETRA_SERVICE) private readonly tetraService: TetraService,
) {}

async execute(command: SendTetraSDSCommand): Promise<void> {
try {
await this.tetraService.sendSDS(
command.issi,
command.message,
command.isFlash,
);
} catch (error) {
throw new SdsNotAbleToSendException(error);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IEvent } from '@nestjs/cqrs';

export class NewTetraStatusEvent implements IEvent {
constructor(
public readonly issi: string,
public readonly status: number,
public readonly sentAt: Date,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class SdsNotAbleToSendException extends Error {
constructor(error: unknown) {
super(`Not able to send SDS: ${error}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const TETRA_SERVICE = Symbol('TETRA_SERVICE');

export interface TetraService {
sendSDS(issi: string, message: string, isFlash?: boolean): Promise<void>;

sendCallOut(
issi: string,
message: string,
noReply: boolean,
prio?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15,
): Promise<void>;

sendStatus(
issi: string,
status: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8,
): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { CommandBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';

import { SendTetraSDSCommand } from '../../core/command/send-tetra-sds.command';
import { SdsNotAbleToSendException } from '../../core/exception/sds-not-able-to-send.exception';
import { PresentableSdsNotSendException } from '../exception/presentable-sds-not-send.exception';
import { RCSResolver } from './rcs.resolver';

describe('RCSResolver', () => {
let resolver: RCSResolver;
let commandBus: DeepMocked<CommandBus>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RCSResolver,
{
provide: CommandBus,
useValue: createMock<CommandBus>(),
},
],
}).compile();

resolver = module.get<RCSResolver>(RCSResolver);
commandBus = module.get<DeepMocked<CommandBus>>(CommandBus);
});

describe('sendSDS', () => {
it('should send a Send Tetra SDS command', async () => {
const issi = '123456';
const message = 'Test message';
const isFlash = true;

await resolver.sendSDS(issi, message, isFlash);

expect(commandBus.execute).toHaveBeenCalledWith(
new SendTetraSDSCommand(issi, message, isFlash),
);
});

it('should throw presentable error on SDS error', async () => {
const issi = '123456';
const message = 'Test message';
const isFlash = false;

commandBus.execute.mockRejectedValueOnce(
new SdsNotAbleToSendException(new Error()),
);

await expect(resolver.sendSDS(issi, message, isFlash)).rejects.toThrow(
PresentableSdsNotSendException,
);

const unknownError = new Error('i am an unknown error');
commandBus.execute.mockRejectedValueOnce(unknownError);

await expect(resolver.sendSDS(issi, message, isFlash)).rejects.toThrow(
unknownError,
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CommandBus } from '@nestjs/cqrs';
import { Args, Mutation, Resolver } from '@nestjs/graphql';

import { SendTetraSDSCommand } from '../../core/command/send-tetra-sds.command';
import { SdsNotAbleToSendException } from '../../core/exception/sds-not-able-to-send.exception';
import { PresentableSdsNotSendException } from '../exception/presentable-sds-not-send.exception';

@Resolver()
export class RCSResolver {
constructor(private readonly commandBus: CommandBus) {}

@Mutation(() => Boolean)
async sendSDS(
@Args('issi') issi: string,
@Args('message') message: string,
@Args('isFlash') isFlash?: boolean,
): Promise<boolean> {
try {
await this.commandBus.execute(
new SendTetraSDSCommand(issi, message, !!isFlash),
);
} catch (error) {
if (error instanceof SdsNotAbleToSendException) {
throw new PresentableSdsNotSendException();
}

throw error;
}

return true;
}
}
Loading

0 comments on commit a12c4e9

Please sign in to comment.