/* * Copyright 2019 gRPC authors. * * Licensed 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 * as assert from 'assert'; import * as http2 from 'http2'; import { range } from 'lodash'; import { Metadata, MetadataObject, MetadataValue } from '../src/metadata'; class TestMetadata extends Metadata { getInternalRepresentation() { return this.internalRepr; } static fromHttp2Headers(headers: http2.IncomingHttpHeaders): TestMetadata { const result = Metadata.fromHttp2Headers(headers) as TestMetadata; result.getInternalRepresentation = TestMetadata.prototype.getInternalRepresentation; return result; } } const validKeyChars = '0123456789abcdefghijklmnopqrstuvwxyz_-.'; const validNonBinValueChars = range(0x20, 0x7f) .map(code => String.fromCharCode(code)) .join(''); describe('Metadata', () => { let metadata: TestMetadata; beforeEach(() => { metadata = new TestMetadata(); }); describe('set', () => { it('Only accepts string values for non "-bin" keys', () => { assert.throws(() => { metadata.set('key', Buffer.from('value')); }); assert.doesNotThrow(() => { metadata.set('key', 'value'); }); }); it('Only accepts Buffer values for "-bin" keys', () => { assert.throws(() => { metadata.set('key-bin', 'value'); }); assert.doesNotThrow(() => { metadata.set('key-bin', Buffer.from('value')); }); }); it('Rejects invalid keys', () => { assert.doesNotThrow(() => { metadata.set(validKeyChars, 'value'); }); assert.throws(() => { metadata.set('key$', 'value'); }, /Error: Metadata key "key\$" contains illegal characters/); assert.throws(() => { metadata.set('', 'value'); }); }); it('Rejects values with non-ASCII characters', () => { assert.doesNotThrow(() => { metadata.set('key', validNonBinValueChars); }); assert.throws(() => { metadata.set('key', 'résumé'); }); }); it('Saves values that can be retrieved', () => { metadata.set('key', 'value'); assert.deepStrictEqual(metadata.get('key'), ['value']); }); it('Overwrites previous values', () => { metadata.set('key', 'value1'); metadata.set('key', 'value2'); assert.deepStrictEqual(metadata.get('key'), ['value2']); }); it('Normalizes keys', () => { metadata.set('Key', 'value1'); assert.deepStrictEqual(metadata.get('key'), ['value1']); metadata.set('KEY', 'value2'); assert.deepStrictEqual(metadata.get('key'), ['value2']); }); }); describe('add', () => { it('Only accepts string values for non "-bin" keys', () => { assert.throws(() => { metadata.add('key', Buffer.from('value')); }); assert.doesNotThrow(() => { metadata.add('key', 'value'); }); }); it('Only accepts Buffer values for "-bin" keys', () => { assert.throws(() => { metadata.add('key-bin', 'value'); }); assert.doesNotThrow(() => { metadata.add('key-bin', Buffer.from('value')); }); }); it('Rejects invalid keys', () => { assert.throws(() => { metadata.add('key$', 'value'); }); assert.throws(() => { metadata.add('', 'value'); }); }); it('Saves values that can be retrieved', () => { metadata.add('key', 'value'); assert.deepStrictEqual(metadata.get('key'), ['value']); }); it('Combines with previous values', () => { metadata.add('key', 'value1'); metadata.add('key', 'value2'); assert.deepStrictEqual(metadata.get('key'), ['value1', 'value2']); }); it('Normalizes keys', () => { metadata.add('Key', 'value1'); assert.deepStrictEqual(metadata.get('key'), ['value1']); metadata.add('KEY', 'value2'); assert.deepStrictEqual(metadata.get('key'), ['value1', 'value2']); }); }); describe('remove', () => { it('clears values from a key', () => { metadata.add('key', 'value'); metadata.remove('key'); assert.deepStrictEqual(metadata.get('key'), []); }); it('Normalizes keys', () => { metadata.add('key', 'value'); metadata.remove('KEY'); assert.deepStrictEqual(metadata.get('key'), []); }); }); describe('get', () => { beforeEach(() => { metadata.add('key', 'value1'); metadata.add('key', 'value2'); metadata.add('key-bin', Buffer.from('value')); }); it('gets all values associated with a key', () => { assert.deepStrictEqual(metadata.get('key'), ['value1', 'value2']); }); it('Normalizes keys', () => { assert.deepStrictEqual(metadata.get('KEY'), ['value1', 'value2']); }); it('returns an empty list for non-existent keys', () => { assert.deepStrictEqual(metadata.get('non-existent-key'), []); }); it('returns Buffers for "-bin" keys', () => { assert.ok(metadata.get('key-bin')[0] instanceof Buffer); }); }); describe('getMap', () => { it('gets a map of keys to values', () => { metadata.add('key1', 'value1'); metadata.add('Key2', 'value2'); metadata.add('KEY3', 'value3a'); metadata.add('KEY3', 'value3b'); assert.deepStrictEqual(metadata.getMap(), { key1: 'value1', key2: 'value2', key3: 'value3a', }); }); }); describe('clone', () => { it('retains values from the original', () => { metadata.add('key', 'value'); const copy = metadata.clone(); assert.deepStrictEqual(copy.get('key'), ['value']); }); it('Does not see newly added values', () => { metadata.add('key', 'value1'); const copy = metadata.clone(); metadata.add('key', 'value2'); assert.deepStrictEqual(copy.get('key'), ['value1']); }); it('Does not add new values to the original', () => { metadata.add('key', 'value1'); const copy = metadata.clone(); copy.add('key', 'value2'); assert.deepStrictEqual(metadata.get('key'), ['value1']); }); it('Copy cannot modify binary values in the original', () => { const buf = Buffer.from('value-bin'); metadata.add('key-bin', buf); const copy = metadata.clone(); const copyBuf = copy.get('key-bin')[0] as Buffer; assert.deepStrictEqual(copyBuf, buf); copyBuf.fill(0); assert.notDeepStrictEqual(copyBuf, buf); }); }); describe('merge', () => { it('appends values from a given metadata object', () => { metadata.add('key1', 'value1'); metadata.add('Key2', 'value2a'); metadata.add('KEY3', 'value3a'); metadata.add('key4', 'value4'); const metadata2 = new TestMetadata(); metadata2.add('KEY1', 'value1'); metadata2.add('key2', 'value2b'); metadata2.add('key3', 'value3b'); metadata2.add('key5', 'value5a'); metadata2.add('key5', 'value5b'); const metadata2IR = metadata2.getInternalRepresentation(); metadata.merge(metadata2); // Ensure metadata2 didn't change assert.deepStrictEqual( metadata2.getInternalRepresentation(), metadata2IR ); assert.deepStrictEqual(metadata.get('key1'), ['value1', 'value1']); assert.deepStrictEqual(metadata.get('key2'), ['value2a', 'value2b']); assert.deepStrictEqual(metadata.get('key3'), ['value3a', 'value3b']); assert.deepStrictEqual(metadata.get('key4'), ['value4']); assert.deepStrictEqual(metadata.get('key5'), ['value5a', 'value5b']); }); }); describe('toHttp2Headers', () => { it('creates an OutgoingHttpHeaders object with expected values', () => { metadata.add('key1', 'value1'); metadata.add('Key2', 'value2'); metadata.add('KEY3', 'value3a'); metadata.add('key3', 'value3b'); metadata.add('key-bin', Buffer.from(range(0, 16))); metadata.add('key-bin', Buffer.from(range(16, 32))); metadata.add('key-bin', Buffer.from(range(0, 32))); const headers = metadata.toHttp2Headers(); assert.deepStrictEqual(headers, { key1: ['value1'], key2: ['value2'], key3: ['value3a', 'value3b'], 'key-bin': [ 'AAECAwQFBgcICQoLDA0ODw==', 'EBESExQVFhcYGRobHB0eHw==', 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=', ], }); }); it('creates an empty header object from empty Metadata', () => { assert.deepStrictEqual(metadata.toHttp2Headers(), {}); }); }); describe('fromHttp2Headers', () => { it('creates a Metadata object with expected values', () => { const headers = { key1: 'value1', key2: ['value2'], key3: ['value3a', 'value3b'], key4: ['part1, part2'], 'key-bin': [ 'AAECAwQFBgcICQoLDA0ODw==', 'EBESExQVFhcYGRobHB0eHw==', 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=', ], }; const metadataFromHeaders = TestMetadata.fromHttp2Headers(headers); const internalRepr = metadataFromHeaders.getInternalRepresentation(); const expected: MetadataObject = new Map([ ['key1', ['value1']], ['key2', ['value2']], ['key3', ['value3a', 'value3b']], ['key4', ['part1, part2']], [ 'key-bin', [ Buffer.from(range(0, 16)), Buffer.from(range(16, 32)), Buffer.from(range(0, 32)), ], ], ]); assert.deepStrictEqual(internalRepr, expected); }); it('creates an empty Metadata object from empty headers', () => { const metadataFromHeaders = TestMetadata.fromHttp2Headers({}); const internalRepr = metadataFromHeaders.getInternalRepresentation(); assert.deepStrictEqual(internalRepr, new Map()); }); }); });