Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 72 additions & 22 deletions src/base64.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterAll, assert, beforeAll, describe, it, vi } from 'vitest';
import { afterAll, expect, beforeAll, describe, it, vi } from 'vitest';

const importBase64 = async () => {
vi.resetModules();
Expand All @@ -7,35 +7,85 @@ const importBase64 = async () => {
return base64;
};

describe('base64', async () => {
let base64 = await importBase64();
const ENCODE_CASES = [
['', ''],
['f', 'Zg=='],
['fo', 'Zm8='],
['foo', 'Zm9v'],
['foob', 'Zm9vYg=='],
['fooba', 'Zm9vYmE='],
['foobar', 'Zm9vYmFy'],
['foo:bar', 'Zm9vOmJhcg=='],
[':', 'Og=='],
['\ufeffa:', '77u/YTo='],
['\0\x7f', 'AH8='],
['ü:密碼', 'w7w65a+G56K8'],
['😀:🔑', '8J+YgDrwn5SR'],
['\ud800', '77+9'],
['\udc00', '77+9'],
] as const;

const DECODE_CASES = [
...ENCODE_CASES.map(
([value, encoded]) => [encoded, value.toWellFormed()] as const,
),
['Zg', 'f'],
['Zm8', 'fo'],
[' \tZm9v\r\nOmJhcg==\f', 'foo:bar'],
['Zh==', 'f'],
['Zm9=', 'fo'],
['/w==', '\ufffd'],
['wyg=', '\ufffd('],
['4oI=', '\ufffd'],
['77+9', '\ufffd'],
] as const;

describe.skipIf(typeof Buffer === 'undefined')('base64 (Buffer)', () => {
let base64: typeof import('./base64.js').base64;

beforeAll(async () => {
base64 = await importBase64();
});

afterAll(() => {
vi.unstubAllGlobals();
vi.resetModules();
});

it.each(ENCODE_CASES)('encodes %j as %j', (value, encoded) => {
expect(base64.encode(value)).toEqual(encoded);
});

it.each(DECODE_CASES)('decodes %j as %j', (encoded, value) => {
expect(base64.decode(encoded)).toEqual(value);
});

describe.skipIf(!Buffer)('Buffer', async () => {
it('should encode base64', () => {
assert.strictEqual(base64.encode('foo:bar'), 'Zm9vOmJhcg==');
});
it.each(ENCODE_CASES)('round-trips %j', (value) => {
expect(base64.decode(base64.encode(value))).toEqual(value.toWellFormed());
});
});

describe.skipIf(!Uint8Array.prototype.toBase64)('base64 (Uint8Array)', () => {
let base64: typeof import('./base64.js').base64;

it('should decode base64', () => {
assert.strictEqual(base64.decode('Zm9vOmJhcg=='), 'foo:bar');
});
beforeAll(async () => {
vi.stubGlobal('Buffer', undefined);
base64 = await importBase64();
});

describe.skipIf(!Uint8Array.prototype.toBase64)('Uint8Array', async () => {
beforeAll(async () => {
vi.stubGlobal('Buffer', undefined);
base64 = await importBase64();
});
afterAll(() => {
vi.unstubAllGlobals();
vi.resetModules();
});

it.each(ENCODE_CASES)('encodes %j as %j', (value, encoded) => {
expect(base64.encode(value)).toEqual(encoded);
});

it('should encode base64', () => {
assert.strictEqual(base64.encode('foo:bar'), 'Zm9vOmJhcg==');
});
it.each(DECODE_CASES)('decodes %j as %j', (encoded, value) => {
expect(base64.decode(encoded)).toEqual(value);
});

it('should decode base64', () => {
assert.strictEqual(base64.decode('Zm9vOmJhcg=='), 'foo:bar');
});
it.each(ENCODE_CASES)('round-trips %j', (value) => {
expect(base64.decode(base64.encode(value))).toEqual(value.toWellFormed());
});
});
2 changes: 1 addition & 1 deletion src/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const base64 = (() => {
}

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true });

return {
encode: (str: string) => {
Expand Down
43 changes: 18 additions & 25 deletions src/format.spec.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,68 @@
import { describe, it, assert } from 'vitest';
import { describe, it, expect } from 'vitest';
import { format } from './index.js';

describe('format(credentials)', function () {
describe('arguments', function () {
describe('credentials', function () {
it('should be required', function () {
assert.throws(() => (format as any)(), /Expected an object/);
expect(() => (format as any)()).toThrow(/Expected an object/);
});

it('should accept credentials', function () {
const header = format({ name: 'foo', pass: 'bar' });
assert.strictEqual(header, 'Basic Zm9vOmJhcg==');
expect(header).toEqual('Basic Zm9vOmJhcg==');
});

it('should reject null', function () {
assert.throws(format.bind(null, null as any), /Expected an object/);
expect(format.bind(null, null as any)).toThrow(/Expected an object/);
});

it('should reject a number', function () {
assert.throws(format.bind(null, 42 as any), /Expected an object/);
expect(format.bind(null, 42 as any)).toThrow(/Expected an object/);
});

it('should reject a string', function () {
assert.throws(format.bind(null, '' as any), /Expected an object/);
expect(format.bind(null, '' as any)).toThrow(/Expected an object/);
});

it('should reject an object without name', function () {
assert.throws(
format.bind(null, { pass: 'bar' } as any),
expect(format.bind(null, { pass: 'bar' } as any)).toThrow(
/Object must have string properties "name" and "pass"/,
);
});

it('should reject an object without pass', function () {
assert.throws(
format.bind(null, { name: 'foo' } as any),
expect(format.bind(null, { name: 'foo' } as any)).toThrow(
/Object must have string properties "name" and "pass"/,
);
});

it('should reject an object with non-string name', function () {
assert.throws(
format.bind(null, { name: 42, pass: 'bar' } as any),
expect(format.bind(null, { name: 42, pass: 'bar' } as any)).toThrow(
/Object must have string properties "name" and "pass"/,
);
});

it('should reject an object with non-string pass', function () {
assert.throws(
format.bind(null, { name: 'foo', pass: 42 } as any),
expect(format.bind(null, { name: 'foo', pass: 42 } as any)).toThrow(
/Object must have string properties "name" and "pass"/,
);
});

it('should reject userid containing colon', function () {
assert.throws(
format.bind(null, { name: 'foo:bar', pass: 'baz' }),
expect(format.bind(null, { name: 'foo:bar', pass: 'baz' })).toThrow(
/must not contain a colon/,
);
});

it('should reject control chars in userid', function () {
assert.throws(
expect(
format.bind(null, { name: 'foo\u0000bar', pass: 'baz' }),
/must not contain control characters/,
);
).toThrow(/must not contain control characters/);
});

it('should reject control chars in password', function () {
assert.throws(
format.bind(null, { name: 'foo', pass: 'bar\u007f' }),
expect(format.bind(null, { name: 'foo', pass: 'bar\u007f' })).toThrow(
/must not contain control characters/,
);
});
Expand All @@ -79,28 +72,28 @@ describe('format(credentials)', function () {
describe('with valid credentials', function () {
it('should return header', function () {
const header = format({ name: 'foo', pass: 'bar' });
assert.strictEqual(header, 'Basic Zm9vOmJhcg==');
expect(header).toEqual('Basic Zm9vOmJhcg==');
});
});

describe('with empty password', function () {
it('should return header', function () {
const header = format({ name: 'foo', pass: '' });
assert.strictEqual(header, 'Basic Zm9vOg==');
expect(header).toEqual('Basic Zm9vOg==');
});
});

describe('with empty userid', function () {
it('should return header', function () {
const header = format({ name: '', pass: 'pass' });
assert.strictEqual(header, 'Basic OnBhc3M=');
expect(header).toEqual('Basic OnBhc3M=');
});
});

describe('with empty userid and pass', function () {
it('should return header', function () {
const header = format({ name: '', pass: '' });
assert.strictEqual(header, 'Basic Og==');
expect(header).toEqual('Basic Og==');
});
});
});
20 changes: 4 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { base64 } from './base64.js';
import { parseBasic } from './utils.js';

/**
* Object to represent user credentials.
Expand All @@ -29,12 +30,11 @@ export function parse(string: string): Credentials | undefined {
throw new TypeError('Expected a string');
}

// parse header
const match = CREDENTIALS_REGEXP.exec(string);
if (!match) return undefined;
const encoded = parseBasic(string);
if (encoded === undefined) return undefined;

// decode user pass
const userPass = base64.decode(match[1]);
const userPass = base64.decode(encoded);
const colonIndex = userPass.indexOf(':');
if (colonIndex === -1) return undefined;

Expand Down Expand Up @@ -79,18 +79,6 @@ export function format(credentials: Credentials): string {
return 'Basic ' + base64.encode(str);
}

/**
* RegExp for basic auth credentials
*
* credentials = auth-scheme 1*SP token68
* auth-scheme = "Basic" ; case insensitive
* token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
* @private
*/

const CREDENTIALS_REGEXP =
/^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/;

/**
* RegExp for RFC 5234 CTL characters (US-ASCII 0-31 and 127).
* @private
Expand Down
Loading
Loading