blob: 6026fbaf87b101b9142c8b97a0741b79cfce5029 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { uuidv7, uuidv4 } from "uuidv7";
import {
SEND_MESSAGES,
type SendMessages,
type SendMessagesConfirmation,
} from "./send-messages.command.js";
import { HeaderValue, HeaderKeyFactory } from "./header.utils.js";
import { DeserializeError } from "../error.utils.js";
const SUCCESS = 0;
const CONFIRMATION_SIZE = 20;
const confirmation = (partitionId: number): SendMessagesConfirmation => ({
streamId: 1,
topicId: 2,
partitionId,
baseOffset: 42n,
});
const serializeConfirmations = (
confirmations: SendMessagesConfirmation[],
): Buffer => {
const b = Buffer.allocUnsafe(4 + confirmations.length * CONFIRMATION_SIZE);
b.writeUInt32LE(confirmations.length, 0);
confirmations.forEach((c, index) => {
const at = 4 + index * CONFIRMATION_SIZE;
b.writeUInt32LE(c.streamId, at);
b.writeUInt32LE(c.topicId, at + 4);
b.writeUInt32LE(c.partitionId, at + 8);
b.writeBigUInt64LE(c.baseOffset, at + 12);
});
return b;
};
const response = (data: Buffer) => ({
status: SUCCESS,
length: data.length,
data,
});
describe("SendMessages", () => {
describe("serialize", () => {
const t1 = {
streamId: 911,
topicId: 213,
messages: [
{ payload: "a" },
{ id: 0, payload: "b" },
{ id: 123, payload: "X" },
{ id: 0n, payload: "c" },
{ id: 1236234534554n, payload: "X" },
{ id: uuidv4(), payload: "d" },
{ id: uuidv7(), payload: "e" },
],
};
it("serialize SendMessages into a buffer", () => {
// metadata length prefix (4) + metadata (18) + batch header (256) +
// 7 frames of 48-byte header + 1-byte payload (343)
assert.deepEqual(SEND_MESSAGES.serialize(t1).length, 621);
});
it("serialize all kinds of messageId", () => {
assert.doesNotThrow(() => SEND_MESSAGES.serialize(t1));
});
it("does not throw on number message id", () => {
const t = { ...t1, messages: [{ id: 42, payload: "m" }] };
assert.doesNotThrow(() => SEND_MESSAGES.serialize(t));
});
it("does not throw on bigint message id", () => {
const t = { ...t1, messages: [{ id: 123n, payload: "m" }] };
assert.doesNotThrow(() => SEND_MESSAGES.serialize(t));
});
it("does not throw on uuid message id", () => {
const t = { ...t1, messages: [{ id: uuidv4(), payload: "uuid" }] };
assert.doesNotThrow(() => SEND_MESSAGES.serialize(t));
});
it("throw on invalid string message id", () => {
const t = { ...t1, messages: [{ id: "foo", payload: "m" }] };
assert.throws(() => SEND_MESSAGES.serialize(t));
});
it("throw on invalid number message id", () => {
const t = { ...t1, messages: [{ id: -12, payload: "n" }] };
assert.throws(() => SEND_MESSAGES.serialize(t));
});
it("throw on invalid bigint message id", () => {
const t = { ...t1, messages: [{ id: -12n, payload: "bn" }] };
assert.throws(() => SEND_MESSAGES.serialize(t));
});
it("serialize message with headers", () => {
const t: SendMessages = {
streamId: 911,
topicId: 213,
messages: [
{
payload: "m",
headers: [
{
key: HeaderKeyFactory.String("p"),
value: HeaderValue.Bool(true),
},
],
},
{
payload: "q",
headers: [
{
key: HeaderKeyFactory.String("v-aze"),
value: HeaderValue.Uint8(128),
},
],
},
{
payload: "x",
headers: [
{
key: HeaderKeyFactory.String("q"),
value: HeaderValue.Double(1 / 3),
},
],
},
{
payload: "s",
headers: [
{
key: HeaderKeyFactory.String("x"),
value: HeaderValue.Uint32(123),
},
],
},
{
payload: "r",
headers: [
{
key: HeaderKeyFactory.String("y"),
value: HeaderValue.Uint64(42n),
},
],
},
{
payload: "g",
headers: [
{
key: HeaderKeyFactory.String("y"),
value: HeaderValue.Float(42.3),
},
],
},
{
payload: "c",
headers: [
{
key: HeaderKeyFactory.String("ID"),
value: HeaderValue.String(uuidv7()),
},
],
},
{
payload: "l",
headers: [
{
key: HeaderKeyFactory.String("val"),
value: HeaderValue.Raw(Buffer.from(uuidv4())),
},
],
},
],
};
assert.doesNotThrow(() => SEND_MESSAGES.serialize(t));
});
});
describe('deserialize', () => {
it('reads one confirmation', () => {
const confirmations = [confirmation(3)];
const r = response(serializeConfirmations(confirmations));
assert.deepEqual(SEND_MESSAGES.deserialize(r), { confirmations });
});
it('reads every confirmation of a multi-partition send', () => {
const confirmations = [confirmation(0), confirmation(1), confirmation(2)];
const r = response(serializeConfirmations(confirmations));
assert.deepEqual(SEND_MESSAGES.deserialize(r), { confirmations });
});
it('reads the wire layout of a confirmation', () => {
const r = response(Buffer.from([
0x01, 0x00, 0x00, 0x00, // count
0x01, 0x00, 0x00, 0x00, // streamId
0x02, 0x00, 0x00, 0x00, // topicId
0x03, 0x00, 0x00, 0x00, // partitionId
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // baseOffset
]));
assert.deepEqual(SEND_MESSAGES.deserialize(r), {
confirmations: [
{ streamId: 1, topicId: 2, partitionId: 3, baseOffset: 4n }
]
});
});
it('reads a committed send that reports no offsets as an empty list', () => {
const r = response(serializeConfirmations([]));
assert.deepEqual(SEND_MESSAGES.deserialize(r), { confirmations: [] });
});
it('reads the bodiless legacy server reply as an empty list', () => {
const r = response(Buffer.alloc(0));
assert.deepEqual(SEND_MESSAGES.deserialize(r), { confirmations: [] });
});
it('throws on a truncated body', () => {
const data = serializeConfirmations([confirmation(0), confirmation(1)]);
for (let i = 1; i < data.length; i += 1)
assert.throws(
() => SEND_MESSAGES.deserialize(response(data.subarray(0, i))),
DeserializeError,
`expected error for truncation at byte ${i}`
);
});
it('throws on trailing bytes', () => {
const data = Buffer.concat([
serializeConfirmations([confirmation(1)]),
Buffer.from([0xFF])
]);
assert.throws(
() => SEND_MESSAGES.deserialize(response(data)),
DeserializeError
);
});
it('throws on a count no body could hold', () => {
const data = Buffer.alloc(4);
data.writeUInt32LE(0xFFFF_FFFF, 0);
assert.throws(
() => SEND_MESSAGES.deserialize(response(data)),
DeserializeError
);
});
});
});