first commit
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { once } = require("events");
|
||||
|
||||
const sharp = require("sharp");
|
||||
|
||||
const app = require("../src/server");
|
||||
|
||||
async function createTestImage() {
|
||||
const width = 200;
|
||||
const height = 200;
|
||||
|
||||
const pixels = Buffer.alloc(
|
||||
width * height * 4
|
||||
);
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < pixels.length;
|
||||
i += 4
|
||||
) {
|
||||
pixels[i] = 128;
|
||||
pixels[i + 1] = 128;
|
||||
pixels[i + 2] = 128;
|
||||
pixels[i + 3] = 255;
|
||||
}
|
||||
|
||||
return sharp(pixels, {
|
||||
raw: {
|
||||
width,
|
||||
height,
|
||||
channels: 4
|
||||
}
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function createServer() {
|
||||
const server = app.listen(0);
|
||||
|
||||
await once(server, "listening");
|
||||
|
||||
const address = server.address();
|
||||
|
||||
return {
|
||||
server,
|
||||
baseUrl: `http://127.0.0.1:${address.port}`
|
||||
};
|
||||
}
|
||||
|
||||
test("GET /api/health", async () => {
|
||||
const {
|
||||
server,
|
||||
baseUrl
|
||||
} = await createServer();
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/health`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
response.status,
|
||||
200
|
||||
);
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
assert.deepEqual(body, {
|
||||
status: "ok"
|
||||
});
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
"encode and decode a message through the API",
|
||||
async () => {
|
||||
const {
|
||||
server,
|
||||
baseUrl
|
||||
} = await createServer();
|
||||
|
||||
try {
|
||||
const image =
|
||||
await createTestImage();
|
||||
|
||||
const encodeForm =
|
||||
new FormData();
|
||||
|
||||
encodeForm.append(
|
||||
"image",
|
||||
new Blob(
|
||||
[image],
|
||||
{
|
||||
type: "image/png"
|
||||
}
|
||||
),
|
||||
"test.png"
|
||||
);
|
||||
|
||||
encodeForm.append(
|
||||
"message",
|
||||
"Hello from the integration test!"
|
||||
);
|
||||
|
||||
encodeForm.append(
|
||||
"password",
|
||||
"test-password"
|
||||
);
|
||||
|
||||
const encodedResponse =
|
||||
await fetch(
|
||||
`${baseUrl}/api/encode`,
|
||||
{
|
||||
method: "POST",
|
||||
body: encodeForm
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
encodedResponse.status,
|
||||
200
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
encodedResponse.headers.get(
|
||||
"content-type"
|
||||
),
|
||||
"image/png"
|
||||
);
|
||||
|
||||
const encodedImage =
|
||||
Buffer.from(
|
||||
await encodedResponse.arrayBuffer()
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
encodedImage.length > 0
|
||||
);
|
||||
|
||||
const decodeForm =
|
||||
new FormData();
|
||||
|
||||
decodeForm.append(
|
||||
"image",
|
||||
new Blob(
|
||||
[encodedImage],
|
||||
{
|
||||
type: "image/png"
|
||||
}
|
||||
),
|
||||
"encoded.png"
|
||||
);
|
||||
|
||||
decodeForm.append(
|
||||
"password",
|
||||
"test-password"
|
||||
);
|
||||
|
||||
const decodedResponse =
|
||||
await fetch(
|
||||
`${baseUrl}/api/decode`,
|
||||
{
|
||||
method: "POST",
|
||||
body: decodeForm
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decodedResponse.status,
|
||||
200
|
||||
);
|
||||
|
||||
const result =
|
||||
await decodedResponse.json();
|
||||
|
||||
assert.deepEqual(
|
||||
result,
|
||||
{
|
||||
type: "message",
|
||||
message:
|
||||
"Hello from the integration test!"
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"encode and decode a file through the API",
|
||||
async () => {
|
||||
const {
|
||||
server,
|
||||
baseUrl
|
||||
} = await createServer();
|
||||
|
||||
try {
|
||||
const image =
|
||||
await createTestImage();
|
||||
|
||||
const originalFile =
|
||||
Buffer.from(
|
||||
"This is a secret file.\n"
|
||||
);
|
||||
|
||||
const encodeForm =
|
||||
new FormData();
|
||||
|
||||
encodeForm.append(
|
||||
"image",
|
||||
new Blob(
|
||||
[image],
|
||||
{
|
||||
type: "image/png"
|
||||
}
|
||||
),
|
||||
"test.png"
|
||||
);
|
||||
|
||||
encodeForm.append(
|
||||
"file",
|
||||
new Blob(
|
||||
[originalFile],
|
||||
{
|
||||
type: "text/plain"
|
||||
}
|
||||
),
|
||||
"secret.txt"
|
||||
);
|
||||
|
||||
encodeForm.append(
|
||||
"password",
|
||||
"test-password"
|
||||
);
|
||||
|
||||
const encodedResponse =
|
||||
await fetch(
|
||||
`${baseUrl}/api/encode`,
|
||||
{
|
||||
method: "POST",
|
||||
body: encodeForm
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
encodedResponse.status,
|
||||
200
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
encodedResponse.headers.get(
|
||||
"content-type"
|
||||
),
|
||||
"image/png"
|
||||
);
|
||||
|
||||
const encodedImage =
|
||||
Buffer.from(
|
||||
await encodedResponse.arrayBuffer()
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
encodedImage.length > 0
|
||||
);
|
||||
|
||||
const decodeForm =
|
||||
new FormData();
|
||||
|
||||
decodeForm.append(
|
||||
"image",
|
||||
new Blob(
|
||||
[encodedImage],
|
||||
{
|
||||
type: "image/png"
|
||||
}
|
||||
),
|
||||
"encoded.png"
|
||||
);
|
||||
|
||||
decodeForm.append(
|
||||
"password",
|
||||
"test-password"
|
||||
);
|
||||
|
||||
const decodedResponse =
|
||||
await fetch(
|
||||
`${baseUrl}/api/decode`,
|
||||
{
|
||||
method: "POST",
|
||||
body: decodeForm
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decodedResponse.status,
|
||||
200
|
||||
);
|
||||
|
||||
const decodedFile =
|
||||
Buffer.from(
|
||||
await decodedResponse.arrayBuffer()
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
decodedFile,
|
||||
originalFile
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decodedResponse.headers.get(
|
||||
"content-type"
|
||||
),
|
||||
"text/plain"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
decodedResponse.headers.get(
|
||||
"content-disposition"
|
||||
),
|
||||
/secret\.txt/
|
||||
);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,514 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const stego = require("../src/stego/engine");
|
||||
|
||||
function createPixels(width, height) {
|
||||
const pixels =
|
||||
Buffer.alloc(
|
||||
width * height * 4
|
||||
);
|
||||
|
||||
// Give the test image valid RGBA values.
|
||||
for (
|
||||
let i = 0;
|
||||
i < pixels.length;
|
||||
i += 4
|
||||
) {
|
||||
pixels[i] = 128;
|
||||
pixels[i + 1] = 128;
|
||||
pixels[i + 2] = 128;
|
||||
pixels[i + 3] = 255;
|
||||
}
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
|
||||
test(
|
||||
"encodes and decodes a text message",
|
||||
async () => {
|
||||
const width = 100;
|
||||
const height = 100;
|
||||
|
||||
const pixels =
|
||||
createPixels(
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
const originalMessage =
|
||||
"kokiiii is the best :3";
|
||||
|
||||
const password =
|
||||
"kokithebestie";
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
message:
|
||||
originalMessage,
|
||||
password
|
||||
});
|
||||
|
||||
const decoded =
|
||||
await stego.decode({
|
||||
pixels:
|
||||
encoded.pixels,
|
||||
password
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
decoded.type,
|
||||
"message"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decoded.message,
|
||||
originalMessage
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"encodes and decodes a file",
|
||||
async () => {
|
||||
const width = 200;
|
||||
const height = 200;
|
||||
|
||||
const pixels =
|
||||
createPixels(
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
const originalFile =
|
||||
Buffer.from(
|
||||
"This is a secret file.\n"
|
||||
);
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
password:
|
||||
"test-password",
|
||||
file: {
|
||||
originalname:
|
||||
"secret.txt",
|
||||
mimetype:
|
||||
"text/plain",
|
||||
buffer:
|
||||
originalFile
|
||||
}
|
||||
});
|
||||
|
||||
const decoded =
|
||||
await stego.decode({
|
||||
pixels:
|
||||
encoded.pixels,
|
||||
password:
|
||||
"test-password"
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
decoded.type,
|
||||
"file"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decoded.filename,
|
||||
"secret.txt"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decoded.mimetype,
|
||||
"text/plain"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
decoded.file,
|
||||
originalFile
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"rejects an incorrect password",
|
||||
async () => {
|
||||
const pixels =
|
||||
createPixels(
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width: 100,
|
||||
height: 100,
|
||||
message:
|
||||
"secret message",
|
||||
password:
|
||||
"correct-password"
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
stego.decode({
|
||||
pixels:
|
||||
encoded.pixels,
|
||||
password:
|
||||
"wrong-password"
|
||||
}),
|
||||
{
|
||||
message:
|
||||
"Invalid password or corrupted payload"
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"requires a password for encrypted payloads",
|
||||
async () => {
|
||||
const pixels =
|
||||
createPixels(
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width: 100,
|
||||
height: 100,
|
||||
message:
|
||||
"encrypted secret",
|
||||
password:
|
||||
"secret-password"
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
stego.decode({
|
||||
pixels:
|
||||
encoded.pixels
|
||||
}),
|
||||
{
|
||||
message:
|
||||
"Password is required for this payload"
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"rejects a missing password during encrypted encoding",
|
||||
async () => {
|
||||
/*
|
||||
* This test intentionally verifies that
|
||||
* passwordless encoding is now allowed.
|
||||
*
|
||||
* It should NOT reject.
|
||||
*/
|
||||
const pixels =
|
||||
createPixels(
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width: 100,
|
||||
height: 100,
|
||||
message:
|
||||
"no password"
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
encoded.pixels
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"encodes and decodes a message without a password",
|
||||
async () => {
|
||||
const pixels =
|
||||
createPixels(
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width: 100,
|
||||
height: 100,
|
||||
message:
|
||||
"passwordless secret"
|
||||
});
|
||||
|
||||
const decoded =
|
||||
await stego.decode({
|
||||
pixels:
|
||||
encoded.pixels
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
decoded.type,
|
||||
"message"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decoded.message,
|
||||
"passwordless secret"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"encodes and decodes a file without a password",
|
||||
async () => {
|
||||
const pixels =
|
||||
createPixels(
|
||||
200,
|
||||
200
|
||||
);
|
||||
|
||||
const originalFile =
|
||||
Buffer.from(
|
||||
"passwordless file"
|
||||
);
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width: 200,
|
||||
height: 200,
|
||||
file: {
|
||||
originalname:
|
||||
"secret.txt",
|
||||
mimetype:
|
||||
"text/plain",
|
||||
buffer:
|
||||
originalFile
|
||||
}
|
||||
});
|
||||
|
||||
const decoded =
|
||||
await stego.decode({
|
||||
pixels:
|
||||
encoded.pixels
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
decoded.type,
|
||||
"file"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decoded.filename,
|
||||
"secret.txt"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
decoded.mimetype,
|
||||
"text/plain"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
decoded.file,
|
||||
originalFile
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"rejects when neither message nor file is supplied",
|
||||
async () => {
|
||||
const pixels =
|
||||
createPixels(
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
stego.encode({
|
||||
pixels,
|
||||
width: 100,
|
||||
height: 100,
|
||||
password:
|
||||
"password"
|
||||
}),
|
||||
{
|
||||
message:
|
||||
"Message or file is required"
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"rejects a payload that is too large",
|
||||
async () => {
|
||||
const pixels =
|
||||
createPixels(
|
||||
10,
|
||||
10
|
||||
);
|
||||
|
||||
const hugeMessage =
|
||||
"A".repeat(10000);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
stego.encode({
|
||||
pixels,
|
||||
width: 10,
|
||||
height: 10,
|
||||
message:
|
||||
hugeMessage,
|
||||
password:
|
||||
"password"
|
||||
}),
|
||||
{
|
||||
message:
|
||||
/Payload too large/
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"reports image capacity",
|
||||
() => {
|
||||
const capacity =
|
||||
stego.getCapacity(
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
capacity,
|
||||
3740
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"rejects a tampered image",
|
||||
async () => {
|
||||
const pixels =
|
||||
createPixels(
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width: 100,
|
||||
height: 100,
|
||||
message:
|
||||
"secret message",
|
||||
password:
|
||||
"test-password"
|
||||
});
|
||||
|
||||
const tampered =
|
||||
Buffer.from(
|
||||
encoded.pixels
|
||||
);
|
||||
|
||||
/*
|
||||
* Flip a bit inside the embedded
|
||||
* payload.
|
||||
*/
|
||||
tampered[100] ^= 1;
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
stego.decode({
|
||||
pixels:
|
||||
tampered,
|
||||
password:
|
||||
"test-password"
|
||||
}),
|
||||
{
|
||||
message:
|
||||
"Invalid password or corrupted payload"
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
test(
|
||||
"rejects a corrupted payload",
|
||||
async () => {
|
||||
const pixels =
|
||||
createPixels(
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
const encoded =
|
||||
await stego.encode({
|
||||
pixels,
|
||||
width: 100,
|
||||
height: 100,
|
||||
message:
|
||||
"secret message",
|
||||
password:
|
||||
"test-password"
|
||||
});
|
||||
|
||||
const corrupted =
|
||||
Buffer.from(
|
||||
encoded.pixels
|
||||
);
|
||||
|
||||
/*
|
||||
* Corrupt several embedded bits
|
||||
* in the payload area.
|
||||
*/
|
||||
for (
|
||||
let i = 100;
|
||||
i < 110;
|
||||
i++
|
||||
) {
|
||||
corrupted[i] ^= 1;
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
stego.decode({
|
||||
pixels:
|
||||
corrupted,
|
||||
password:
|
||||
"test-password"
|
||||
}),
|
||||
{
|
||||
message:
|
||||
"Invalid password or corrupted payload"
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user