initial release
[squeep-api-dingus] / test / lib / content-negotiation.js
1 /* eslint-disable capitalized-comments */
2 /* eslint-env mocha */
3 'use strict';
4
5 const assert = require('assert');
6 const ContentNegotiation = require('../../lib/content-negotiation');
7 const Enum = require('../../lib/enum');
8
9 const TYPES = {
10 Any: '*/*',
11 ImageAny: 'image/*',
12 ImagePng: 'image/png',
13 TextAny: 'text/*',
14 TextHtml: 'text/html',
15 TextPlain: 'text/plain',
16 AppJson: 'application/json',
17 };
18
19 describe('ContentNegotiation', function () {
20 describe('_unpackAcceptClause', function () {
21 it('handles bare type', function () {
22 const clause = TYPES.TextPlain;
23 const expected = {
24 type: TYPES.TextPlain,
25 weight: 1.0,
26 params: {},
27 };
28 const result = ContentNegotiation._unpackAcceptClause(clause);
29 assert.deepStrictEqual(result, expected);
30 });
31 it('handles weighted type', function () {
32 const clause = ` ${TYPES.TextPlain}; q=0.2`;
33 const expected = {
34 type: TYPES.TextPlain,
35 weight: 0.2,
36 params: {},
37 };
38 const result = ContentNegotiation._unpackAcceptClause(clause);
39 assert.deepStrictEqual(result, expected);
40 });
41 it('handles type with extension parameters', function () {
42 const clause = `${TYPES.TextPlain}; format=flowed;charset=UTF-8`;
43 const expected = {
44 type: TYPES.TextPlain,
45 weight: 1.0,
46 params: {
47 format: 'flowed',
48 charset: 'UTF-8',
49 },
50 };
51 const result = ContentNegotiation._unpackAcceptClause(clause);
52 assert.deepStrictEqual(result, expected);
53 });
54 it('handles type with params and weight', function () {
55 const clause = `${TYPES.AppJson};charset=UTF-8;q=0.123;ext=local`;
56 const expected = {
57 type: TYPES.AppJson,
58 weight: 0.123,
59 params: {
60 charset: 'UTF-8',
61 ext: 'local',
62 },
63 };
64 const result = ContentNegotiation._unpackAcceptClause(clause);
65 assert.deepStrictEqual(result, expected);
66 });
67 it('ignores empty type', function () {
68 const clause = '';
69 const expected = undefined;
70 const result = ContentNegotiation._unpackAcceptClause(clause);
71 assert.deepStrictEqual(result, expected);
72 });
73 it('ignores weird param', function () {
74 const clause = `${TYPES.AppJson};blah`;
75 const expected = {
76 type: TYPES.AppJson,
77 weight: 1.0,
78 params: {},
79 };
80 const result = ContentNegotiation._unpackAcceptClause(clause);
81 assert.deepStrictEqual(result, expected);
82 });
83 }); // _unpackAcceptClause
84
85 describe('_acceptClauses', function () {
86 it('orders multiple types properly', function () {
87 const acceptHeader = 'text/*;q=0.3, text/html;q=0.7, text/html;level=1,text/html;level=2;q=0.4, */*;q=0.5';
88 const expected = [
89 { type: TYPES.TextHtml, weight: 1.0, params: { 'level': '1' } },
90 { type: TYPES.TextHtml, weight: 0.7, params: {} },
91 { type: TYPES.Any, weight: 0.4998, params: {} }, // N.B. Implementation detail
92 { type: TYPES.TextHtml, weight: 0.4, params: { 'level': '2' } },
93 { type: TYPES.TextAny, weight: 0.2999, params: {} }, // N.B. Implementation detail
94 ];
95 const result = ContentNegotiation._acceptClauses(acceptHeader);
96 assert.deepStrictEqual(result, expected);
97 });
98 it('does the expected', function () {
99 const acceptHeader = 'text/*;q=0.5, text/plain;q=0.7, */*;q=0.2';
100 const expected = [
101 { type: TYPES.TextPlain, weight: 0.7, params: {} },
102 { type: TYPES.TextAny, weight: 0.4999, params: {} }, // N.B. Implementation detail
103 { type: TYPES.Any, weight: 0.1998, params: {} }, // N.B. Implementation detail
104 ];
105 const result = ContentNegotiation._acceptClauses(acceptHeader);
106 assert.deepStrictEqual(result, expected);
107 });
108 it('covers missing data', function () {
109 const result = ContentNegotiation._acceptClauses(undefined);
110 assert.deepStrictEqual(result, []);
111 });
112 }); // _acceptClauses
113
114 describe('_matchType', function () {
115 it('matches simmple', function () {
116 const result = ContentNegotiation._matchType(TYPES.TextPlain, TYPES.TextPlain);
117 assert.strictEqual(result, true);
118 });
119 it('does not match simmple', function () {
120 const result = ContentNegotiation._matchType(TYPES.TextPlain, TYPES.TextHtml);
121 assert.strictEqual(result, false);
122 });
123 it('matches subtype wildcard', function () {
124 const result = ContentNegotiation._matchType(TYPES.ImageAny, TYPES.ImagePng);
125 assert.strictEqual(result, true);
126 });
127 it('does not match subtype wildcard', function () {
128 const result = ContentNegotiation._matchType(TYPES.ImageAny, TYPES.TextPlain);
129 assert.strictEqual(result, false);
130 });
131 it('matches any wildcard', function () {
132 const result = ContentNegotiation._matchType(TYPES.Any, TYPES.AppJson);
133 assert.strictEqual(result, true);
134 });
135 it('matches main type', function () {
136 const result = ContentNegotiation._matchType('image', TYPES.ImagePng);
137 assert.strictEqual(result, true);
138 });
139 it('does not match main type', function () {
140 const result = ContentNegotiation._matchType('image', TYPES.AppJson);
141 assert.strictEqual(result, false);
142 });
143 }); // _matchType
144
145 describe('accept', function () {
146 it('accepts anything', function () {
147 const types = [ TYPES.AppJson, TYPES.TextPlain ];
148 const acceptHeader = undefined;
149 const result = ContentNegotiation.accept(types, acceptHeader);
150 assert.strictEqual(result, TYPES.AppJson);
151 });
152
153 it('picks the most acceptible type', function () {
154 const types = [ TYPES.AppJson, TYPES.TextPlain ];
155 const acceptHeader = 'text/*;q=0.5, text/html;q=0.6, text/plain;q=0.7, */*;q=0.2, image/png;q=0.0, image/gif;q=0.0';
156 const result = ContentNegotiation.accept(types, acceptHeader);
157 assert.strictEqual(result, TYPES.TextPlain);
158 });
159 it('picks the most acceptible type', function () {
160 const types = [ TYPES.AppJson, TYPES.TextPlain ];
161 const acceptHeader = 'image/png;q=0.0, image/gif;q=0.0, text/html;q=0.6, text/*;q=0.5, text/plain;q=0.7, */*;q=0.7';
162 const result = ContentNegotiation.accept(types, acceptHeader);
163 assert.strictEqual(result, TYPES.TextPlain);
164 });
165 it('refuses to pick a zero-weight type', function () {
166 const types = [ TYPES.AppJson, TYPES.TextPlain ];
167 const acceptHeader = 'application/*;q=0.0, unhandled/type;q=0.7, none/none;q=0.2';
168 const result = ContentNegotiation.accept(types, acceptHeader);
169 assert.strictEqual(result, undefined);
170 });
171 it('picks least-wild match', function () {
172 const types = [ TYPES.TextHtml, TYPES.TextPlain ];
173 const acceptHeader = '*/*;q=0.5, text/plain;q=0.5, text/*;q=0.5';
174 const result = ContentNegotiation.accept(types, acceptHeader);
175 assert.strictEqual(result, TYPES.TextPlain);
176 });
177 }); // accept
178
179 describe('preferred', function () {
180 it('falls back to identity', function () {
181 const encodings = [];
182 const header = 'br, gzip';
183 const result = ContentNegotiation.preferred(encodings, header);
184 assert.deepStrictEqual(result, [Enum.EncodingType.Identity]);
185 });
186 it('picks appropriate', function () {
187 const encodings = ['br', 'identity'];
188 const header = 'lz4, br, gzip;q=.5';
189 const result = ContentNegotiation.preferred(encodings, header);
190 assert.deepStrictEqual(result, [Enum.EncodingType.Brotli, Enum.EncodingType.Identity]);
191 });
192 it('respects forbidding', function () {
193 const encodings = ['br'];
194 const header = 'gzip, *;q=0';
195 const result = ContentNegotiation.preferred(encodings, header);
196 assert.deepStrictEqual(result, []);
197 });
198 it('respects identity', function () {
199 const encodings = ['br'];
200 const header = 'gzip, identity;q=0.1, *;q=0';
201 const result = ContentNegotiation.preferred(encodings, header);
202 assert.deepStrictEqual(result, [Enum.EncodingType.Identity]);
203 });
204 }); // preferred
205
206 }); // ContentNegotiation
207