chore(express-partial-content): integrate tests and convert to vitest

This commit is contained in:
Elian Doran 2025-05-03 02:31:20 +03:00
parent d6736821b6
commit c1a5b1a0aa
No known key found for this signature in database
8 changed files with 279 additions and 210 deletions

View File

@ -1,182 +0,0 @@
import * as utils from "../src/utils";
import * as ParseRangeHeaderExports from "../src/parseRangeHeader";
import { ContentDoesNotExistError } from "../src/ContentDoesNotExistError";
import { SinonSandbox, createSandbox, SinonStub, SinonSpy } from "sinon";
import { createPartialContentHandler } from "../src/createPartialContentHandler";
import { ContentProvider } from "../src/ContentProvider";
import { Logger } from "../src/Logger";
import { expect } from "chai";
import { Request, Response } from "express";
import { Content } from "../src/Content";
import { Stream } from "stream";
import { Range } from "../src/Range";
describe("createPartialContentHandler tests", () => {
let sandbox: SinonSandbox;
let logger: Logger;
beforeEach(() => {
sandbox = createSandbox();
logger = {
debug: sandbox.stub() as (message: string, extra?: any) => void
};
});
afterEach(() => {
sandbox.restore();
});
it("returns a handler", () => {
const contentProvider = sandbox.stub().resolves({}) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
expect(typeof handler === "function");
});
describe("handler tests", () => {
let req: Request;
let res: Response;
let statusSpy: SinonSpy;
let sendSpy: SinonSpy;
let sendStatusSpy: SinonSpy;
beforeEach(() => {
req = {} as Request;
res = {
status: (code: number) => res,
send: (message: string) => res,
sendStatus: (code: number) => res,
setHeader: sandbox.stub() as (name: string, value: string) => void
} as Response;
statusSpy = sandbox.spy(res, "status");
sendSpy = sandbox.spy(res, "send");
sendStatusSpy = sandbox.spy(res, "sendStatus");
});
it("invokes contentProvider with the specified request", async () => {
const contentProvider = sandbox.stub().resolves({}) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
try {
await handler(req, res);
} catch {}
expect((contentProvider as SinonStub).calledOnceWith(req));
});
it("returns 404 if contentProvider throws ContentDoesNotExistError error", async () => {
const error = new ContentDoesNotExistError("404-File not found!");
const contentProvider = sandbox.stub().rejects(error) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
try {
await handler(req, res);
expect(statusSpy.calledOnceWith(404));
expect(sendSpy.calledOnceWith(error.message));
} catch {
expect(false);
}
});
it("returns 500 if contentProvider throws any other error", async () => {
const error = new Error("Something went wrong!");
const contentProvider = sandbox.stub().rejects(error) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
try {
await handler(req, res);
expect(sendStatusSpy.calledOnceWith(500));
} catch {
expect(false);
}
});
it("returns 416 if parseRangeHeader throws RangeParserError error", async () => {
const contentProvider = sandbox.stub().resolves({}) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
req.headers = { range: "bytes=30-10" };
try {
await handler(req, res);
expect(statusSpy.calledOnceWith(416));
} catch {
expect(false);
}
});
it("returns 500 if parseRangeHeader throws other errors", async () => {
const parseRangeHeaderStub = sandbox
.stub(ParseRangeHeaderExports, "parseRangeHeader")
.throws(new Error("Something went wrong!"));
const contentProvider = sandbox.stub().resolves({}) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
try {
await handler(req, res);
expect(sendStatusSpy.calledOnceWith(500));
} catch {
expect(false);
}
});
it("returns correct response if range is not specified", async () => {
const result = ({
pipe() {
return result;
}
} as any) as Stream;
const content: Content = {
fileName: "file.txt",
totalSize: 10,
mimeType: "text/plain",
getStream(range?: Range) {
return result;
}
};
const pipeSpy = sandbox.spy(result, "pipe");
const getStreamSpy = sandbox.spy(content, "getStream");
const contentProvider = sandbox.stub().resolves(content) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
const setContentTypeHeaderSpy = sandbox.spy(utils, "setContentTypeHeader");
const setContentDispositionHeaderSpy = sandbox.spy(utils, "setContentDispositionHeader");
const setAcceptRangesHeaderSpy = sandbox.spy(utils, "setAcceptRangesHeader");
const setContentLengthHeaderSpy = sandbox.spy(utils, "setContentLengthHeader");
const setContentRangeHeaderSpy = sandbox.spy(utils, "setContentRangeHeader");
try {
await handler(req, res);
expect(setContentTypeHeaderSpy.calledOnceWith(content.mimeType, res));
expect(setContentDispositionHeaderSpy.calledOnceWith(content.fileName, res));
expect(setAcceptRangesHeaderSpy.calledOnceWith(res));
expect(setContentLengthHeaderSpy.calledOnceWith(content.totalSize, res));
expect(getStreamSpy.calledOnceWith());
expect(pipeSpy.calledOnceWith(res));
expect(setContentRangeHeaderSpy.notCalled);
} catch {
expect(false);
}
});
it("returns correct partial response if range is specified", async () => {
req.headers = {
range: "bytes=0-5"
};
const result = ({
pipe() {
return result;
}
} as any) as Stream;
const content: Content = {
fileName: "file.txt",
totalSize: 10,
mimeType: "text/plain",
getStream(range?: Range) {
return result;
}
};
const range = { start: 0, end: 5 };
const pipeSpy = sandbox.spy(result, "pipe");
const getStreamSpy = sandbox.spy(content, "getStream");
const contentProvider = sandbox.stub().resolves(content) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
const setContentTypeHeaderSpy = sandbox.spy(utils, "setContentTypeHeader");
const setContentDispositionHeaderSpy = sandbox.spy(utils, "setContentDispositionHeader");
const setAcceptRangesHeaderSpy = sandbox.spy(utils, "setAcceptRangesHeader");
const setContentLengthHeaderSpy = sandbox.spy(utils, "setContentLengthHeader");
const setContentRangeHeaderSpy = sandbox.spy(utils, "setContentRangeHeader");
try {
await handler(req, res);
expect(setContentTypeHeaderSpy.calledOnceWith(content.mimeType, res));
expect(setContentDispositionHeaderSpy.calledOnceWith(content.fileName, res));
expect(setAcceptRangesHeaderSpy.calledOnceWith(res));
expect(setContentRangeHeaderSpy.calledOnceWith(range, content.totalSize, res));
expect(setContentLengthHeaderSpy.calledOnceWith(6, res));
expect(getStreamSpy.calledOnceWith(range));
expect(pipeSpy.calledOnceWith(res));
} catch {
expect(false);
}
});
});
});

View File

@ -0,0 +1,181 @@
import * as utils from "./utils.js";
import * as ParseRangeHeaderExports from "./parseRangeHeader.js";
import { ContentDoesNotExistError } from "./ContentDoesNotExistError.js";
import { createPartialContentHandler } from "./createPartialContentHandler.js";
import type { ContentProvider } from "./ContentProvider.js";
import type { Logger } from "./Logger.js";
import type { Request, Response } from "express";
import type { Content } from "./Content.js";
import { Stream } from "stream";
import type { Range } from "./Range.js";
import type { MockInstance } from "vitest";
describe("createPartialContentHandler tests", () => {
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn() as (message: string, extra?: any) => void
};
});
afterEach(() => {
vi.restoreAllMocks();
});
it("returns a handler", () => {
const contentProvider = vi.fn().mockResolvedValue({}) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
expect(typeof handler === "function");
});
describe("handler tests", () => {
let req: Request;
let res: Response;
let statusSpy: MockInstance;
let sendSpy: MockInstance;
let sendStatusSpy: MockInstance;
beforeEach(() => {
req = {} as Request;
res = {
status: (code: number) => res,
send: (message: string) => res,
sendStatus: (code: number) => res,
setHeader: vi.fn() as (name: string, value: string) => void
} as Response;
statusSpy = vi.spyOn(res, "status");
sendSpy = vi.spyOn(res, "send");
sendStatusSpy = vi.spyOn(res, "sendStatus");
});
it("invokes contentProvider with the specified request", async () => {
const contentProvider = vi.fn().mockResolvedValue({}) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
try {
await handler(req, res);
} catch {}
expect(contentProvider).toHaveBeenCalledExactlyOnceWith(req);
});
it("returns 404 if contentProvider throws ContentDoesNotExistError error", async () => {
const error = new ContentDoesNotExistError("404-File not found!");
const contentProvider = vi.fn().mockRejectedValue(error) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
try {
await handler(req, res);
expect(statusSpy).toHaveBeenCalledExactlyOnceWith(404);
expect(sendSpy).toHaveBeenCalledExactlyOnceWith(error.message);
} catch {
expect(false);
}
});
it("returns 500 if contentProvider throws any other error", async () => {
const error = new Error("Something went wrong!");
const contentProvider = vi.fn().mockRejectedValue(error) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
try {
await handler(req, res);
expect(sendStatusSpy).toHaveBeenCalledExactlyOnceWith(500);
} catch {
expect(false);
}
});
it("returns 416 if parseRangeHeader throws RangeParserError error", async () => {
const contentProvider = vi.fn().mockResolvedValue({}) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
req.headers = { range: "bytes=30-10" };
try {
await handler(req, res);
expect(statusSpy).toHaveBeenCalledExactlyOnceWith(416);
} catch {
expect(false);
}
});
it("returns 500 if parseRangeHeader throws other errors", async () => {
const parseRangeHeaderStub = vi
.spyOn(ParseRangeHeaderExports, "parseRangeHeader")
.mockImplementation(() => {
throw new Error("Something went wrong!")
});
const contentProvider = vi.fn().mockResolvedValue({}) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
try {
await handler(req, res);
expect(sendStatusSpy).toHaveBeenCalledExactlyOnceWith(500);
} catch {
expect(false);
}
});
it("returns correct response if range is not specified", async () => {
const result = ({
pipe() {
return result;
}
} as any) as Stream;
const content: Content = {
fileName: "file.txt",
totalSize: 10,
mimeType: "text/plain",
getStream(range?: Range) {
return result;
}
};
const pipeSpy = vi.spyOn(result, "pipe");
const getStreamSpy = vi.spyOn(content, "getStream");
const contentProvider = vi.fn().mockResolvedValue(content) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
const setContentTypeHeaderSpy = vi.spyOn(utils, "setContentTypeHeader");
const setContentDispositionHeaderSpy = vi.spyOn(utils, "setContentDispositionHeader");
const setAcceptRangesHeaderSpy = vi.spyOn(utils, "setAcceptRangesHeader");
const setContentLengthHeaderSpy = vi.spyOn(utils, "setContentLengthHeader");
const setContentRangeHeaderSpy = vi.spyOn(utils, "setContentRangeHeader");
try {
await handler(req, res);
expect(setContentTypeHeaderSpy).toHaveBeenCalledExactlyOnceWith(content.mimeType, res);
expect(setContentDispositionHeaderSpy).toHaveBeenCalledExactlyOnceWith(content.fileName, res);
expect(setAcceptRangesHeaderSpy).toHaveBeenCalledExactlyOnceWith(res);
expect(setContentLengthHeaderSpy).toHaveBeenCalledExactlyOnceWith(content.totalSize, res);
expect(getStreamSpy).toHaveBeenCalledExactlyOnceWith();
expect(pipeSpy).toHaveBeenCalledExactlyOnceWith(res);
expect(setContentRangeHeaderSpy).not.toHaveBeenCalled();
} catch {
expect(false);
}
});
it("returns correct partial response if range is specified", async () => {
req.headers = {
range: "bytes=0-5"
};
const result = ({
pipe() {
return result;
}
} as any) as Stream;
const content: Content = {
fileName: "file.txt",
totalSize: 10,
mimeType: "text/plain",
getStream(range?: Range) {
return result;
}
};
const range = { start: 0, end: 5 };
const pipeSpy = vi.spyOn(result, "pipe");
const getStreamSpy = vi.spyOn(content, "getStream");
const contentProvider = vi.fn().mockResolvedValue(content) as ContentProvider;
const handler = createPartialContentHandler(contentProvider, logger);
const setContentTypeHeaderSpy = vi.spyOn(utils, "setContentTypeHeader");
const setContentDispositionHeaderSpy = vi.spyOn(utils, "setContentDispositionHeader");
const setAcceptRangesHeaderSpy = vi.spyOn(utils, "setAcceptRangesHeader");
const setContentLengthHeaderSpy = vi.spyOn(utils, "setContentLengthHeader");
const setContentRangeHeaderSpy = vi.spyOn(utils, "setContentRangeHeader");
try {
await handler(req, res);
expect(setContentTypeHeaderSpy).toHaveBeenCalledExactlyOnceWith(content.mimeType, res);
expect(setContentDispositionHeaderSpy).toHaveBeenCalledExactlyOnceWith(content.fileName, res);
expect(setAcceptRangesHeaderSpy).toHaveBeenCalledExactlyOnceWith(res);
expect(setContentRangeHeaderSpy).toHaveBeenCalledExactlyOnceWith(range, content.totalSize, res);
expect(setContentLengthHeaderSpy).toHaveBeenCalledExactlyOnceWith(6, res);
expect(getStreamSpy).toHaveBeenCalledExactlyOnceWith(range);
expect(pipeSpy).toHaveBeenCalledExactlyOnceWith(res);
} catch {
expect(false);
}
});
});
});

View File

@ -1,20 +1,16 @@
import { parseRangeHeader } from "../src/parseRangeHeader"; import { parseRangeHeader } from "./parseRangeHeader.js";
import { SinonSandbox, createSandbox } from "sinon"; import type { Logger } from "./Logger.js";
import { Logger } from "../src/Logger"; import { RangeParserError } from "./RangeParserError.js";
import { expect } from "chai";
import { RangeParserError } from "../src/RangeParserError";
describe("parseRangeHeader tests", () => { describe("parseRangeHeader tests", () => {
let sandbox: SinonSandbox;
let logger: Logger; let logger: Logger;
beforeEach(() => { beforeEach(() => {
sandbox = createSandbox();
logger = { logger = {
debug: sandbox.stub() as (message: string, extra?: any) => void debug: vi.fn() as (message: string, extra?: any) => void
}; };
}); });
afterEach(() => { afterEach(() => {
sandbox.restore(); vi.restoreAllMocks();
}); });
it("returns null if range is not specified", () => { it("returns null if range is not specified", () => {
let value = parseRangeHeader("", 10, logger); let value = parseRangeHeader("", 10, logger);

View File

@ -1,6 +1,5 @@
import { Request, Response } from "express"; import type { Request, Response } from "express";
import { expect } from "chai"; import { expect, type Mock } from "vitest";
import sinon, { SinonStub, SinonSpy } from "sinon";
import { import {
getHeader, getHeader,
@ -12,7 +11,7 @@ import {
setContentDispositionHeader, setContentDispositionHeader,
setContentRangeHeader, setContentRangeHeader,
setCacheControlHeaderNoCache setCacheControlHeaderNoCache
} from "../src/utils"; } from "./utils.js";
describe("utils tests", () => { describe("utils tests", () => {
let req: Request; let req: Request;
@ -25,7 +24,7 @@ describe("utils tests", () => {
} }
} as Request; } as Request;
res = { res = {
setHeader: sinon.stub() as (name: string, value: string) => void setHeader: vi.fn() as (name: string, value: string) => void
} as Response; } as Response;
}); });
describe("getHeader tests", () => { describe("getHeader tests", () => {
@ -43,7 +42,7 @@ describe("utils tests", () => {
const name = "Content-Type"; const name = "Content-Type";
const value = "application/octet-stream"; const value = "application/octet-stream";
setHeader(name, value, res); setHeader(name, value, res);
expect((res.setHeader as SinonStub).calledOnceWith(name, value)); expect(res.setHeader).toHaveBeenCalledExactlyOnceWith(name, value);
}); });
}); });
describe("getRangeHeader tests", () => { describe("getRangeHeader tests", () => {
@ -56,34 +55,36 @@ describe("utils tests", () => {
it("sets Content-Type header with specified value", () => { it("sets Content-Type header with specified value", () => {
const value = "application/octet-stream"; const value = "application/octet-stream";
setContentTypeHeader(value, res); setContentTypeHeader(value, res);
expect((res.setHeader as SinonStub).calledOnceWith("Content-Type", value)); expect(res.setHeader).toHaveBeenCalledExactlyOnceWith("Content-Type", value);
}); });
}); });
describe("setContentLengthHeader tests", () => { describe("setContentLengthHeader tests", () => {
it("sets Content-Length header with specified value", () => { it("sets Content-Length header with specified value", () => {
const value = 100; const value = "100";
setContentLengthHeader(value, res); setContentLengthHeader(value, res);
expect((res.setHeader as SinonStub).calledOnceWith("Content-Length", value)); expect(res.setHeader).toHaveBeenCalledExactlyOnceWith("Content-Length", value);
}); });
}); });
describe("setAcceptRangesHeader tests", () => { describe("setAcceptRangesHeader tests", () => {
it("sets Accept-Ranges header with specified value", () => { it("sets Accept-Ranges header with specified value", () => {
const value = "bytes"; const value = "bytes";
setAcceptRangesHeader(res); setAcceptRangesHeader(res);
expect((res.setHeader as SinonStub).calledOnceWith("Accept-Ranges", value)); expect(res.setHeader).toHaveBeenCalledExactlyOnceWith("Accept-Ranges", value);
}); });
}); });
describe("setContentRangeHeader tests", () => { describe("setContentRangeHeader tests", () => {
it("sets Content-Range header with specified value", () => { it("sets Content-Range header with specified value", () => {
let range = { start: 10, end: 100 }; let range: { start: number, end: number } | null = { start: 10, end: 100 };
const size = 1000; const size = 1000;
let value = `bytes ${range.start}-${range.end}/${size}`; let value = `bytes ${range.start}-${range.end}/${size}`;
setContentRangeHeader(range, size, res); setContentRangeHeader(range, size, res);
expect((res.setHeader as SinonStub).calledOnceWith("Content-Range", value)); expect(res.setHeader).toHaveBeenCalledExactlyOnceWith("Content-Range", value);
(res.setHeader as Mock).mockReset();
range = null; range = null;
value = `bytes */${size}`; value = `bytes */${size}`;
setContentRangeHeader(range, size, res); setContentRangeHeader(range, size, res);
expect((res.setHeader as SinonStub).calledOnceWith("Content-Range", value)); expect(res.setHeader).toHaveBeenCalledExactlyOnceWith("Content-Range", value);
}); });
}); });
describe("setContentDispositionHeader tests", () => { describe("setContentDispositionHeader tests", () => {
@ -91,20 +92,20 @@ describe("utils tests", () => {
const fileName = "file.txt"; const fileName = "file.txt";
const value = `attachment; filename*=utf-8''${fileName}`; const value = `attachment; filename*=utf-8''${fileName}`;
setContentDispositionHeader(fileName, res); setContentDispositionHeader(fileName, res);
expect((res.setHeader as SinonStub).calledOnceWith("Content-Disposition", value)).to.be.true; expect(res.setHeader).toHaveBeenCalledExactlyOnceWith("Content-Disposition", value);
}); });
it("sets Content-Disposition header with specified unicode", () => { it("sets Content-Disposition header with specified unicode", () => {
const fileName = "file.txt"; const fileName = "file.txt";
const value = `attachment; filename*=utf-8''${encodeURIComponent(fileName)}`; const value = `attachment; filename*=utf-8''${encodeURIComponent(fileName)}`;
setContentDispositionHeader(fileName, res); setContentDispositionHeader(fileName, res);
expect((res.setHeader as SinonStub).calledOnceWith("Content-Disposition", value)).to.be.true; expect(res.setHeader).toHaveBeenCalledExactlyOnceWith("Content-Disposition", value);
}); });
}); });
describe("setCacheControlHeaderNoCache tests", () => { describe("setCacheControlHeaderNoCache tests", () => {
it("sets Cache-Control header with specified value", () => { it("sets Cache-Control header with specified value", () => {
const value = "no-cache"; const value = "no-cache";
setCacheControlHeaderNoCache(res); setCacheControlHeaderNoCache(res);
expect((res.setHeader as SinonStub).calledOnceWith("Cache-Control", value)); expect(res.setHeader).toHaveBeenCalledExactlyOnceWith("Cache-Control", value);
}); });
}); });
}); });

View File

@ -5,6 +5,9 @@
"references": [ "references": [
{ {
"path": "./tsconfig.lib.json" "path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
} }
] ]
} }

View File

@ -6,8 +6,26 @@
"outDir": "dist", "outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"emitDeclarationOnly": false, "emitDeclarationOnly": false,
"types": ["node"] "types": [
"node"
]
}, },
"include": ["src/**/*.ts"], "include": [
"references": [] "src/**/*.ts"
],
"references": [],
"exclude": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
]
} }

View File

@ -0,0 +1,29 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts",
"src/**/*.ts"
]
}

View File

@ -0,0 +1,23 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/express-partial-content',
plugins: [],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));