11 files changed
+116
-29
No files matched your search
@@ -1,7 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { normalizeRabbitmqAddresses, parseRabbitmqAddress } from "@/lib/connection/rabbitmqAddresses";
|
||||
|
||||
describe("RabbitMQ addresses", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("keeps comma-separated addresses", () => {
|
||||
expect(normalizeRabbitmqAddresses("node1:5672, node2:5672")).toBe("node1:5672,node2:5672");
|
||||
});
|
||||
@@ -11,12 +15,36 @@ describe("RabbitMQ addresses", () => {
|
||||
expect(normalizeRabbitmqAddresses("node1, node2:5673")).toBe("node1:5672,node2:5673");
|
||||
});
|
||||
|
||||
it("keeps IPv4 addresses with custom ports", () => {
|
||||
expect(normalizeRabbitmqAddresses("192.0.2.176:51016")).toBe("192.0.2.176:51016");
|
||||
expect(parseRabbitmqAddress("192.0.2.176:51016")).toEqual({ host: "192.0.2.176", port: 51016 });
|
||||
});
|
||||
|
||||
it("parses IPv4 addresses without the browser URL implementation", () => {
|
||||
vi.stubGlobal(
|
||||
"URL",
|
||||
class {
|
||||
constructor() {
|
||||
throw new Error("custom schemes are unsupported");
|
||||
}
|
||||
},
|
||||
);
|
||||
expect(normalizeRabbitmqAddresses("192.0.2.176:51016")).toBe("192.0.2.176:51016");
|
||||
expect(parseRabbitmqAddress("192.0.2.176:51016")).toEqual({ host: "192.0.2.176", port: 51016 });
|
||||
});
|
||||
|
||||
it("normalizes common address separators to commas", () => {
|
||||
expect(normalizeRabbitmqAddresses("node1:5672;node2:5672,node3:5672\nnode4:5672 node5:5672")).toBe("node1:5672,node2:5672,node3:5672,node4:5672,node5:5672");
|
||||
});
|
||||
|
||||
it("keeps IPv6 addresses", () => {
|
||||
expect(normalizeRabbitmqAddresses("[::1]:5672;[2001:db8::1]:5672")).toBe("[::1]:5672,[2001:db8::1]:5672");
|
||||
expect(parseRabbitmqAddress("[::1]:5672")).toEqual({ host: "::1", port: 5672 });
|
||||
});
|
||||
|
||||
it("appends the default port to IPv6 addresses without a port", () => {
|
||||
expect(normalizeRabbitmqAddresses("[::1]")).toBe("[::1]:5672");
|
||||
expect(parseRabbitmqAddress("[::1]")).toEqual({ host: "::1", port: 5672 });
|
||||
});
|
||||
|
||||
it("rejects empty addresses", () => {
|
||||
@@ -30,4 +58,10 @@ describe("RabbitMQ addresses", () => {
|
||||
it("rejects invalid address values", () => {
|
||||
expect(() => normalizeRabbitmqAddresses("node1:5672/path,node2:5672")).toThrow("RabbitMQ addresses are invalid");
|
||||
});
|
||||
|
||||
it("rejects invalid ports", () => {
|
||||
expect(() => normalizeRabbitmqAddresses("node1:")).toThrow("RabbitMQ addresses are invalid");
|
||||
expect(() => normalizeRabbitmqAddresses("node1:0")).toThrow("RabbitMQ addresses are invalid");
|
||||
expect(() => normalizeRabbitmqAddresses("node1:65536")).toThrow("RabbitMQ addresses are invalid");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,12 @@
|
||||
const RABBITMQ_ADDRESS_SEPARATOR = /[\s,;,;]+/u;
|
||||
const RABBITMQ_DEFAULT_PORT = "5672";
|
||||
const RABBITMQ_PORT_PATTERN = /^\d+$/u;
|
||||
const RABBITMQ_INVALID_HOST_CHARACTER = /[\s/?#@%\\\u200b-\u200d\ufeff]/u;
|
||||
|
||||
export interface RabbitmqAddress {
|
||||
host: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
function requireRabbitmqAddresses(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
@@ -7,20 +14,66 @@ function requireRabbitmqAddresses(value: string): string {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function normalizeRabbitmqAddress(address: string): string {
|
||||
function invalidRabbitmqAddresses(): never {
|
||||
throw new Error("RabbitMQ addresses are invalid");
|
||||
}
|
||||
|
||||
function validateRabbitmqPort(port: string): void {
|
||||
if (!RABBITMQ_PORT_PATTERN.test(port)) invalidRabbitmqAddresses();
|
||||
const parsed = Number(port);
|
||||
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) invalidRabbitmqAddresses();
|
||||
}
|
||||
|
||||
function validateRabbitmqHostname(host: string): void {
|
||||
if (!host || RABBITMQ_INVALID_HOST_CHARACTER.test(host)) invalidRabbitmqAddresses();
|
||||
}
|
||||
|
||||
function validateRabbitmqIpv6(host: string): void {
|
||||
try {
|
||||
const parsed = new URL(`http://[${host}]`);
|
||||
if (!parsed.hostname || parsed.username || parsed.password || parsed.port || parsed.search || parsed.hash || parsed.pathname !== "/") {
|
||||
invalidRabbitmqAddresses();
|
||||
}
|
||||
} catch {
|
||||
invalidRabbitmqAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRabbitmqAddress(address: string): RabbitmqAddress {
|
||||
if (address.includes("://")) {
|
||||
throw new Error("RabbitMQ addresses must be host:port values without a URL scheme");
|
||||
}
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(`amqp://${address}`);
|
||||
} catch {
|
||||
throw new Error("RabbitMQ addresses are invalid");
|
||||
|
||||
if (address.startsWith("[")) {
|
||||
const match = address.match(/^\[([^\]]+)\](?::([^:]+))?$/u);
|
||||
if (!match) invalidRabbitmqAddresses();
|
||||
const [, host, port] = match;
|
||||
validateRabbitmqIpv6(host);
|
||||
if (port) {
|
||||
validateRabbitmqPort(port);
|
||||
return { host, port: Number(port) };
|
||||
}
|
||||
return { host, port: Number(RABBITMQ_DEFAULT_PORT) };
|
||||
}
|
||||
if (!parsed.hostname || parsed.username || parsed.password || parsed.search || parsed.hash || (parsed.pathname && parsed.pathname !== "/")) {
|
||||
throw new Error("RabbitMQ addresses are invalid");
|
||||
|
||||
if (address.includes("[") || address.includes("]")) invalidRabbitmqAddresses();
|
||||
const separator = address.indexOf(":");
|
||||
if (separator !== address.lastIndexOf(":")) invalidRabbitmqAddresses();
|
||||
const host = separator >= 0 ? address.slice(0, separator) : address;
|
||||
const port = separator >= 0 ? address.slice(separator + 1) : "";
|
||||
if (!host || (separator >= 0 && !port)) invalidRabbitmqAddresses();
|
||||
validateRabbitmqHostname(host);
|
||||
if (port) {
|
||||
validateRabbitmqPort(port);
|
||||
return { host, port: Number(port) };
|
||||
}
|
||||
return parsed.port ? address : `${address}:${RABBITMQ_DEFAULT_PORT}`;
|
||||
return { host, port: Number(RABBITMQ_DEFAULT_PORT) };
|
||||
}
|
||||
|
||||
function normalizeRabbitmqAddress(address: string): string {
|
||||
const parsed = parseRabbitmqAddress(address);
|
||||
const hasExplicitPort = address.startsWith("[") ? address.includes("]:") : address.includes(":");
|
||||
return hasExplicitPort ? address : `${address}:${parsed.port}`;
|
||||
}
|
||||
|
||||
export function normalizeRabbitmqAddresses(value: string): string {
|
||||
|
||||
Reference in new issue
Block a user