Skip to content

Commit

Permalink
add some tests for server modules
Browse files Browse the repository at this point in the history
  • Loading branch information
fquffio committed Feb 7, 2025
1 parent e9545e7 commit 5deebc5
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 7 deletions.
7 changes: 1 addition & 6 deletions src/lib/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,4 @@ export type Hashed<T> = { hash: string } & T;
* @param input String to compute hash for.
* @param algo Algorithm.
*/
export const computeHash = (input: string, algo = 'sha256'): string => {
const hash = createHash(algo);
hash.update(input);

return hash.digest('hex');
};
export const computeHash = (input: string, algo = 'sha256'): string => createHash(algo).update(input).digest('hex');
242 changes: 242 additions & 0 deletions tests/server/sitemap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { describe, expect, it } from 'vitest';
import { Sitemap, SitemapIndex } from '$lib/server/sitemap';
import { xml2js } from 'xml-js';
import { gunzipSync } from 'node:zlib';

const toBuf = (body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) => Buffer.from(body.value ?? []);

describe(Sitemap.name, () => {
const baseUrl = new URL('https://www.example.com/');

it('should throw an error when adding duplicate URLs if duplicates are not allowed', () => {
const sitemap = new Sitemap(baseUrl);
sitemap.append({ loc: '/foo' }, false);

expect(() => sitemap.append({ loc: '/foo' }, false)).to.throw(
'Location /foo had already been added to this sitemap: duplicate URLs are not allowed',
);
expect(() => sitemap.append({ loc: '/foo' }, true)).to.not.throw();
});

it('should build an empty sitemap', async () => {
const sitemap = new Sitemap(baseUrl);

const xml = sitemap.toString();
const { elements } = xml2js(xml);
expect(elements).deep.equal([
{
attributes: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' },
name: 'urlset',
type: 'element',
},
]);

const uncompressed = sitemap.toResponse(false);
expect(uncompressed)
.to.be.instanceOf(Response)
.that.satisfies(
(response: Response) =>
response.headers.get('content-type') === 'application/xml' && !response.headers.has('content-encoding'),
);
await expect(uncompressed.body?.getReader().read())
.to.be.a('promise')
.that.resolves.satisfies(
(body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) => toBuf(body)?.toString('utf8') === xml,
);

const compressed = sitemap.toResponse(true);
expect(compressed)
.to.be.instanceOf(Response)
.that.satisfies(
(response: Response) =>
response.headers.get('content-type') === 'application/xml' &&
response.headers.get('content-encoding') === 'gzip',
);
await expect(compressed.body?.getReader().read())
.to.be.a('promise')
.that.resolves.satisfies(
(body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) =>
toBuf(body).toString('utf8') !== xml && gunzipSync(toBuf(body)).toString('utf8') === xml,
);
});

it('should build a sitemap with a few URLs', async () => {
const sitemap = new Sitemap(baseUrl)
.append({ loc: '/foo', changeFreq: 'daily' })
.append({ loc: '/foo', priority: 42 })
.append({ loc: '/bar' })
.append({ loc: '/baz', changeFreq: 'monthly', priority: 1, lastMod: new Date('2025-01-01T00:00:00') });

const xml = sitemap.toString();
const { elements } = xml2js(xml);
expect(elements).deep.equal([
{
attributes: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' },
name: 'urlset',
type: 'element',
elements: [
{
name: 'url',
type: 'element',
elements: [
{ name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/foo' }] },
{ name: 'changefreq', type: 'element', elements: [{ type: 'text', text: 'daily' }] },
],
},
{
name: 'url',
type: 'element',
elements: [
{ name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/bar' }] },
],
},
{
name: 'url',
type: 'element',
elements: [
{ name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/baz' }] },
{ name: 'lastmod', type: 'element', elements: [{ type: 'text', text: '2025-01-01' }] },
{ name: 'changefreq', type: 'element', elements: [{ type: 'text', text: 'monthly' }] },
{ name: 'priority', type: 'element', elements: [{ type: 'text', text: '1.00' }] },
],
},
],
},
]);

const uncompressed = sitemap.toResponse(false);
expect(uncompressed)
.to.be.instanceOf(Response)
.that.satisfies(
(response: Response) =>
response.headers.get('content-type') === 'application/xml' && !response.headers.has('content-encoding'),
);
await expect(uncompressed.body?.getReader().read())
.to.be.a('promise')
.that.resolves.satisfies(
(body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) => toBuf(body)?.toString('utf8') === xml,
);

const compressed = sitemap.toResponse(true);
expect(compressed)
.to.be.instanceOf(Response)
.that.satisfies(
(response: Response) =>
response.headers.get('content-type') === 'application/xml' &&
response.headers.get('content-encoding') === 'gzip',
);
await expect(compressed.body?.getReader().read())
.to.be.a('promise')
.that.resolves.satisfies(
(body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) =>
toBuf(body).toString('utf8') !== xml && gunzipSync(toBuf(body)).toString('utf8') === xml,
);
});
});

describe(SitemapIndex.name, () => {
const baseUrl = new URL('https://www.example.com/');

it('should build an empty sitemap index', async () => {
const sitemapIndex = new SitemapIndex(baseUrl);

const xml = sitemapIndex.toString();
const { elements } = xml2js(xml);
expect(elements).deep.equal([
{
attributes: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' },
name: 'sitemapindex',
type: 'element',
},
]);

const uncompressed = sitemapIndex.toResponse(false);
expect(uncompressed)
.to.be.instanceOf(Response)
.that.satisfies(
(response: Response) =>
response.headers.get('content-type') === 'application/xml' && !response.headers.has('content-encoding'),
);
await expect(uncompressed.body?.getReader().read())
.to.be.a('promise')
.that.resolves.satisfies(
(body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) => toBuf(body)?.toString('utf8') === xml,
);

const compressed = sitemapIndex.toResponse(true);
expect(compressed)
.to.be.instanceOf(Response)
.that.satisfies(
(response: Response) =>
response.headers.get('content-type') === 'application/xml' &&
response.headers.get('content-encoding') === 'gzip',
);
await expect(compressed.body?.getReader().read())
.to.be.a('promise')
.that.resolves.satisfies(
(body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) =>
toBuf(body).toString('utf8') !== xml && gunzipSync(toBuf(body)).toString('utf8') === xml,
);
});

it('should build a sitemap with a few URLs', async () => {
const sitemapIndex = new SitemapIndex(baseUrl)
.append({ loc: '/foo.xml' })
.append({ loc: '/bar.xml', lastMod: new Date('2025-01-01T00:00:00') });

const xml = sitemapIndex.toString();
const { elements } = xml2js(xml);
expect(elements).deep.equal([
{
attributes: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' },
name: 'sitemapindex',
type: 'element',
elements: [
{
name: 'sitemap',
type: 'element',
elements: [
{ name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/foo.xml' }] },
],
},
{
name: 'sitemap',
type: 'element',
elements: [
{ name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/bar.xml' }] },
{ name: 'lastmod', type: 'element', elements: [{ type: 'text', text: '2025-01-01' }] },
],
},
],
},
]);

const uncompressed = sitemapIndex.toResponse(false);
expect(uncompressed)
.to.be.instanceOf(Response)
.that.satisfies(
(response: Response) =>
response.headers.get('content-type') === 'application/xml' && !response.headers.has('content-encoding'),
);
await expect(uncompressed.body?.getReader().read())
.to.be.a('promise')
.that.resolves.satisfies(
(body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) => toBuf(body)?.toString('utf8') === xml,
);

const compressed = sitemapIndex.toResponse(true);
expect(compressed)
.to.be.instanceOf(Response)
.that.satisfies(
(response: Response) =>
response.headers.get('content-type') === 'application/xml' &&
response.headers.get('content-encoding') === 'gzip',
);
await expect(compressed.body?.getReader().read())
.to.be.a('promise')
.that.resolves.satisfies(
(body: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>) =>
toBuf(body).toString('utf8') !== xml && gunzipSync(toBuf(body)).toString('utf8') === xml,
);
});
});
91 changes: 91 additions & 0 deletions tests/server/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { computeHash, secureId, withTmpDir } from '$lib/server/utils';
import { existsSync, statSync } from 'node:fs';
import { basename } from 'node:path';
import { describe, expect, it } from 'vitest';

describe(secureId.name, () => {
const CASES = {
'should generate a 32 bytes random hash': { bytes: 32, expectedLength: 64 },
'should generate a 18 bytes random hash': { bytes: 18, expectedLength: 36 },
} satisfies Record<string, { bytes: number; expectedLength: number }>;

Object.entries(CASES).forEach(([label, { bytes, expectedLength }]) =>
it(label, () => {
expect(secureId(bytes)).to.be.a('string').with.length(expectedLength);
}),
);
});

describe(withTmpDir.name, () => {
it('should create a temporary directory and remove it upon completion', async () => {
expect.assertions(3);

const expected = Symbol();
let tmpDir: string | undefined = undefined;
const result = withTmpDir('foo-', (path) => {
tmpDir = path;
expect(path)
.to.be.a('string')
.that.satisfies((path: string) => basename(path).startsWith('foo-'))
.and.satisfies((path: string) => existsSync(path) && statSync(path).isDirectory());

return expected;
});

await expect(result).resolves.equal(expected);

expect(tmpDir).satisfy((path: string) => !existsSync(path));
});

it('should create a temporary directory and remove it after an error is thrown', async () => {
expect.assertions(3);

const expected = new Error('my error');
let tmpDir: string | undefined = undefined;
const result = withTmpDir('foo-', (path) => {
tmpDir = path;
expect(path)
.to.be.a('string')
.that.satisfies((path: string) => basename(path).startsWith('foo-'))
.and.satisfies((path: string) => existsSync(path) && statSync(path).isDirectory());

throw expected;
});

await expect(result).rejects.equal(expected);

expect(tmpDir).satisfy((path: string) => !existsSync(path));
});
});

describe(computeHash.name, () => {
const CASES = {
'should compute md5': {
input: 'password',
expected: '5f4dcc3b5aa765d61d8327deb882cf99',
algorithm: 'md5',
},
'should compute sha1': {
input: 'foo bar',
expected: '3773dea65156909838fa6c22825cafe090ff8030',
algorithm: 'sha1',
},
'should compute sha256': {
input: 'hello world',
expected: 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9',
algorithm: 'sha256',
},
'should compute sha512': {
input: 'example string',
expected:
'f63ffbf293e2631e013dc2a0958f54f6797f096c36adda6806f717e1d4a314c0fb443ec71eec73cfbd8efa1ad2c709b902066e6356396b97a7ea5191de349012',
algorithm: 'sha512',
},
} satisfies Record<string, { input: string; expected: string; algorithm: string }>;

Object.entries(CASES).forEach(([label, { input, algorithm, expected }]) =>
it(label, () => {
expect(computeHash(input, algorithm)).equals(expected);
}),
);
});
2 changes: 1 addition & 1 deletion vitest.workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default defineWorkspace([
{
extends: './vitest.config.ts',
test: {
include: ['tests/**/*.{test,spec}.ts', '!tests/**/*.server.{test,spec}.ts'],
include: ['tests/**/*.{test,spec}.ts', '!tests/**/*.server.{test,spec}.ts', '!tests/server/**/*.{test,spec}.ts'],
name: 'browser',
browser: {
enabled: true,
Expand Down

0 comments on commit 5deebc5

Please sign in to comment.