feat: lots of stuff
This commit is contained in:
73
tests/mqtt.test.ts
Normal file
73
tests/mqtt.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, beforeEach, afterEach, it, vi, expect } from 'vitest';
|
||||
|
||||
import { createWorld, type World } from './utils/utils.world.ts';
|
||||
import { statements } from './utils/utils.statements.ts';
|
||||
|
||||
describe('mqtt', () => {
|
||||
let world: World = undefined as unknown as World;
|
||||
|
||||
beforeEach(async () => {
|
||||
world = await createWorld({});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (world) {
|
||||
await world.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should be able to send messages to all subscribers', async () => {
|
||||
const [clientA, clientB, clientC] = await world.connect(statements.all, statements.all, statements.all);
|
||||
const spyB = vi.fn();
|
||||
const spyC = vi.fn();
|
||||
clientB.on('message', spyB);
|
||||
clientC.on('message', spyC);
|
||||
await clientB.subscribeAsync('test');
|
||||
await clientC.subscribeAsync('test');
|
||||
await clientA.publishAsync('test', 'test');
|
||||
await vi.waitUntil(() => spyB.mock.calls.length && spyC.mock.calls.length);
|
||||
|
||||
expect(spyB).toHaveBeenCalledTimes(1);
|
||||
expect(spyC).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not be able to subscribe if not allowed', async () => {
|
||||
const [client] = await world.connect([]);
|
||||
const promise = client.subscribeAsync('test');
|
||||
await expect(promise).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should not be able to publish if not allowed', async () => {
|
||||
const [client] = await world.connect([]);
|
||||
const promise = client.publishAsync('test', 'test');
|
||||
|
||||
// TODO: why does this not throw?
|
||||
// await expect(promise).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should not be able to read messages if not allowed', async () => {
|
||||
const [clientA, clientB] = await world.connect(statements.all, statements.noRead);
|
||||
const spy = vi.fn();
|
||||
clientB.on('message', spy);
|
||||
await clientB.subscribeAsync('test');
|
||||
await clientA.publishAsync('test', 'test');
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should be able to handle many connections', async () => {
|
||||
const clients = await world.connect(...new Array(50).fill(statements.all));
|
||||
const spies = await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
const spy = vi.fn();
|
||||
client.on('message', spy);
|
||||
await client.subscribeAsync('test');
|
||||
return spy;
|
||||
}),
|
||||
);
|
||||
const [sender] = await world.connect(statements.all);
|
||||
await sender.publishAsync('test', 'test');
|
||||
await vi.waitUntil(() => spies.every((s) => s.mock.calls.length));
|
||||
});
|
||||
});
|
||||
4
tests/tsconfig.json
Normal file
4
tests/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["../src/**/*.ts", "./**/*.ts"]
|
||||
}
|
||||
25
tests/utils/utils.statements.ts
Normal file
25
tests/utils/utils.statements.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Statement } from '#root/access/access.schemas.ts';
|
||||
|
||||
const statements = {
|
||||
all: [
|
||||
{
|
||||
effect: 'allow',
|
||||
resources: ['**'],
|
||||
actions: ['**'],
|
||||
},
|
||||
],
|
||||
noRead: [
|
||||
{
|
||||
effect: 'allow',
|
||||
resources: ['**'],
|
||||
actions: ['**'],
|
||||
},
|
||||
{
|
||||
effect: 'disallow',
|
||||
resources: ['**'],
|
||||
actions: ['mqtt:read'],
|
||||
},
|
||||
],
|
||||
} satisfies Record<string, Statement[]>;
|
||||
|
||||
export { statements };
|
||||
73
tests/utils/utils.world.ts
Normal file
73
tests/utils/utils.world.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import mqtt, { connectAsync, MqttClient } from 'mqtt';
|
||||
import getPort from 'get-port';
|
||||
|
||||
import { AccessHandler } from '#root/access/access.handler.ts';
|
||||
import { type Statement } from '#root/access/access.schemas.ts';
|
||||
import { AccessTokens } from '#root/access/access.token.ts';
|
||||
import { MqttServer } from '#root/server/server.ts';
|
||||
import type { TopicDefinition } from '#root/topics/topcis.schemas.ts';
|
||||
import { TopicsHandler } from '#root/topics/topics.handler.ts';
|
||||
import { TopicsStore } from '#root/topics/topics.store.ts';
|
||||
|
||||
type CreateSocketOptions = {
|
||||
port: number;
|
||||
token: string;
|
||||
};
|
||||
|
||||
const createSocket = async (options: CreateSocketOptions) => {
|
||||
const { port, token } = options;
|
||||
const mqttClient = await connectAsync(`ws://localhost:${port}/ws`, {
|
||||
username: 'token',
|
||||
password: token,
|
||||
reconnectOnConnackError: false,
|
||||
});
|
||||
return mqttClient;
|
||||
};
|
||||
|
||||
type WorldOptions = {
|
||||
topics?: TopicDefinition[];
|
||||
};
|
||||
|
||||
const createWorld = async (options: WorldOptions) => {
|
||||
const { topics = [] } = options;
|
||||
const secret = 'test';
|
||||
const accessHandler = new AccessHandler();
|
||||
const accessTokens = new AccessTokens({
|
||||
secret,
|
||||
});
|
||||
accessHandler.register('token', accessTokens);
|
||||
const topicsHandler = new TopicsHandler();
|
||||
const topicsStore = new TopicsStore();
|
||||
topicsStore.register(...topics);
|
||||
topicsHandler.register(topicsStore);
|
||||
const server = new MqttServer({ topicsHandler, accessHandler });
|
||||
const fastify = await server.getHttpServer();
|
||||
const port = await getPort();
|
||||
await fastify.listen({ port });
|
||||
const sockets: MqttClient[] = [];
|
||||
return {
|
||||
connect: async (...clients: Statement[][]) => {
|
||||
const newSockets = await Promise.all(
|
||||
clients.map((statements) =>
|
||||
createSocket({
|
||||
port,
|
||||
token: accessTokens.generate({
|
||||
statements,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
sockets.push(...newSockets);
|
||||
return newSockets;
|
||||
},
|
||||
destroy: async () => {
|
||||
await Promise.all(sockets.map((s) => s.endAsync()));
|
||||
await fastify.close();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type World = Awaited<ReturnType<typeof createWorld>>;
|
||||
|
||||
export type { World };
|
||||
export { createWorld };
|
||||
Reference in New Issue
Block a user