1 /*******************************************************************************
2  * Copyright (c) 2005, 2019 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.osgi.tests.security;
15 
16 import java.io.File;
17 import java.io.FileOutputStream;
18 import java.io.IOException;
19 import java.io.OutputStream;
20 import java.net.URL;
21 import java.security.KeyStore;
22 import java.security.KeyStoreException;
23 import java.security.cert.Certificate;
24 import java.security.cert.CertificateException;
25 import java.util.ArrayList;
26 import junit.framework.Test;
27 import junit.framework.TestCase;
28 import junit.framework.TestSuite;
29 import org.eclipse.osgi.internal.service.security.KeyStoreTrustEngine;
30 import org.eclipse.osgi.service.security.TrustEngine;
31 import org.eclipse.osgi.tests.OSGiTestsActivator;
32 
33 public class KeyStoreTrustEngineTest extends TestCase {
34 
35 	private static char[] PASSWORD_DEFAULT = { 'c', 'h', 'a', 'n', 'g', 'e', 'i', 't' };
36 	private static String TYPE_DEFAULT = "JKS"; //$NON-NLS-1$
37 
38 	private static TestCase[] s_tests = {
39 			/* findTrustAnchor tests */
40 			new KeyStoreTrustEngineTest("findTrustAnchor positive test: self signed trusted", "ca1_root") { //$NON-NLS-1$ //$NON-NLS-2$
41 				public void runTest() {
42 					testFindTrustAnchor0();
43 				}
44 			}, new KeyStoreTrustEngineTest("findTrustAnchor positive test: chain with root trusted", "ca1_root") { //$NON-NLS-1$ //$NON-NLS-2$
45 				public void runTest() {
46 					testFindTrustAnchor1();
47 				}
48 			}, new KeyStoreTrustEngineTest("findTrustAnchor positive test: chain with intermediate trusted", "ca1_ou") { //$NON-NLS-1$ //$NON-NLS-2$
49 				public void runTest() {
50 					testFindTrustAnchor2();
51 				}
52 			}, new KeyStoreTrustEngineTest("findTrustAnchor positive test: chain with leaf trusted", "ca1_leafb") { //$NON-NLS-1$ //$NON-NLS-2$
53 				public void runTest() {
54 					testFindTrustAnchor3();
55 				}
56 			}, new KeyStoreTrustEngineTest("findTrustAnchor negative test: untrusted self signed") { //$NON-NLS-1$
57 				public void runTest() {
58 					testFindTrustAnchor4();
59 				}
60 			}, new KeyStoreTrustEngineTest("findTrustAnchor negative test: untrusted chain") { //$NON-NLS-1$
61 				public void runTest() {
62 					testFindTrustAnchor5();
63 				}
64 			}, new KeyStoreTrustEngineTest("findTrustAnchor negative test: invalid chain") { //$NON-NLS-1$
65 				public void runTest() {
66 					testFindTrustAnchor6();
67 				}
68 			}, new KeyStoreTrustEngineTest("findTrustAnchor negative test: incomplete-able chain") { //$NON-NLS-1$
69 				public void runTest() {
70 					testFindTrustAnchor7();
71 				}
72 			}, new KeyStoreTrustEngineTest("findTrustAnchor negative test: null chain") { //$NON-NLS-1$
73 				public void runTest() {
74 					testFindTrustAnchor8();
75 				}
76 			},
77 			/* addTrustAnchor tests */
78 			new KeyStoreTrustEngineTest("addTrustAnchor positive test: add with alias") { //$NON-NLS-1$
79 				public void runTest() {
80 					testAddTrustAnchor0();
81 				}
82 			}, /*
83 				 * , new
84 				 * KeyStoreTrustEngineTest("addTrustAnchor positive test: add with autogenerated alias"
85 				 * , null) { public void runTest() { testAddTrustAnchor1(); } }
86 				 */
87 			new KeyStoreTrustEngineTest("addTrustAnchor negative test: null cert specified") { //$NON-NLS-1$
88 				public void runTest() {
89 					testAddTrustAnchor2();
90 				}
91 			}, new KeyStoreTrustEngineTest("addTrustAnchor negative test: existing cert specified", "ca1_root") { //$NON-NLS-1$ //$NON-NLS-2$
92 				public void runTest() {
93 					testAddTrustAnchor3();
94 				}
95 			}, new KeyStoreTrustEngineTest("addTrustAnchor negative test: existing alias specified", "ca1_root") { //$NON-NLS-1$ //$NON-NLS-2$
96 				public void runTest() {
97 					testAddTrustAnchor4();
98 				}
99 			}
100 			/* removeTrustAnchor tests */
101 			, new KeyStoreTrustEngineTest("removeTrustAnchor positive test: remove by alias", "ca1_root") { //$NON-NLS-1$ //$NON-NLS-2$
102 				public void runTest() {
103 					testRemoveTrustAnchor0();
104 				}
105 			}, new KeyStoreTrustEngineTest("removeTrustAnchor positive test: remove by cert", "ca1_root") { //$NON-NLS-1$ //$NON-NLS-2$
106 				public void runTest() {
107 					testRemoveTrustAnchor1();
108 				}
109 			}, new KeyStoreTrustEngineTest("removeTrustAnchor negative test: cert not found") { //$NON-NLS-1$
110 				public void runTest() {
111 					testRemoveTrustAnchor2();
112 				}
113 			}, new KeyStoreTrustEngineTest("removeTrustAnchor negative test: by alias not found") { //$NON-NLS-1$
114 				public void runTest() {
115 					testRemoveTrustAnchor3();
116 				}
117 			}, new KeyStoreTrustEngineTest("removeTrustAnchor negative test: remove by null alias") { //$NON-NLS-1$
118 				public void runTest() {
119 					testRemoveTrustAnchor4();
120 				}
121 			}, new KeyStoreTrustEngineTest("removeTrustAnchor negative test: remove by null certificate") { //$NON-NLS-1$
122 				public void runTest() {
123 					testRemoveTrustAnchor5();
124 				}
125 			},
126 			/* getTrustAnchor tests */
127 			new KeyStoreTrustEngineTest("getTrustAnchor positive test: get by alias", "ca1_root") { //$NON-NLS-1$ //$NON-NLS-2$
128 				public void runTest() {
129 					testGetTrustAnchor0();
130 				}
131 			}, new KeyStoreTrustEngineTest("getTrustAnchor negative test: get by null alias") { //$NON-NLS-1$
132 				public void runTest() {
133 					testGetTrustAnchor1();
134 				}
135 			}, new KeyStoreTrustEngineTest("getTrustAnchor negative test: does not exist") { //$NON-NLS-1$
136 				public void runTest() {
137 					testGetTrustAnchor2();
138 				}
139 			},
140 			/* getAliases tests */
141 			new KeyStoreTrustEngineTest("getAliases positive test: get the alias list", "ca1_root", "ca2_root") { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
142 				public void runTest() {
143 					testGetAliases0();
144 				}
145 			} };
146 
suite()147 	public static Test suite() {
148 		TestSuite suite = new TestSuite("Unit tests for TrustEngine"); //$NON-NLS-1$
149 		for (TestCase s_test : s_tests) {
150 			suite.addTest(s_test);
151 		}
152 		return suite;
153 	}
154 
155 	private static KeyStore supportStore;
156 	static {
157 		try {
158 			URL supportUrl = OSGiTestsActivator.getContext().getBundle().getEntry("test_files/security/keystore.jks"); //$NON-NLS-1$
159 			supportStore = KeyStore.getInstance(TYPE_DEFAULT);
supportUrl.openStream()160 			supportStore.load(supportUrl.openStream(), PASSWORD_DEFAULT);
161 		} catch (Exception e) {
162 			e.printStackTrace();
163 		}
164 	}
165 
166 	private String[] aliases;
167 	private KeyStore testStore;
168 	private File testStoreFile;
169 	TrustEngine engine;
170 
KeyStoreTrustEngineTest()171 	public KeyStoreTrustEngineTest() {
172 		// placeholder
173 	}
174 
KeyStoreTrustEngineTest(String name, String... aliases)175 	public KeyStoreTrustEngineTest(String name, String... aliases) {
176 		super(name);
177 		this.aliases = aliases;
178 	}
179 
setUp()180 	protected void setUp() throws Exception {
181 		if (supportStore == null) {
182 			fail("Could not open keystore with test certificates!"); //$NON-NLS-1$
183 		}
184 
185 		testStore = KeyStore.getInstance(TYPE_DEFAULT);
186 		testStore.load(null, PASSWORD_DEFAULT);
187 		if (aliases != null) {
188 			for (String alias : aliases) {
189 				testStore.setCertificateEntry(alias, getTestCertificate(alias));
190 			}
191 		}
192 		testStoreFile = File.createTempFile("teststore", "jks"); //$NON-NLS-1$ //$NON-NLS-2$
193 		final FileOutputStream out = new FileOutputStream(testStoreFile);
194 		try {
195 			testStore.store(out, PASSWORD_DEFAULT);
196 		} finally {
197 			safeClose(out);
198 		}
199 		engine = new KeyStoreTrustEngine(testStoreFile.getPath(), TYPE_DEFAULT, PASSWORD_DEFAULT, "teststore", null); //$NON-NLS-1$
200 	}
201 
202 	/**
203 	 * Closes a stream and ignores any resulting exception. This is useful when
204 	 * doing stream cleanup in a finally block where secondary exceptions are not
205 	 * worth logging.
206 	 */
safeClose(OutputStream out)207 	protected static void safeClose(OutputStream out) {
208 		try {
209 			if (out != null)
210 				out.close();
211 		} catch (IOException e) {
212 			// ignore
213 		}
214 	}
215 
tearDown()216 	protected void tearDown() {
217 		engine = null;
218 		testStore = null;
219 		testStoreFile.delete();
220 	}
221 
getTestCertificate(String alias)222 	private static Certificate getTestCertificate(String alias) throws KeyStoreException {
223 		return supportStore.getCertificate(alias);
224 	}
225 
getTestCertificateChain(String... aliases)226 	private static Certificate[] getTestCertificateChain(String... aliases) throws KeyStoreException {
227 		ArrayList<Certificate> certs = new ArrayList<>(aliases.length);
228 		for (String alias : aliases) {
229 			certs.add(getTestCertificate(alias));
230 		}
231 		return certs.toArray(new Certificate[] {});
232 	}
233 
234 	// findTrustAnchor positive test: self signed trusted
testFindTrustAnchor0()235 	public void testFindTrustAnchor0() {
236 		try {
237 			Certificate cert = engine.findTrustAnchor(new Certificate[] { getTestCertificate("ca1_root") }); //$NON-NLS-1$
238 			assertNotNull("Did not return a cert for self-signed case", cert); //$NON-NLS-1$
239 			assertEquals("Input and output certs not equal for self-signed case", cert, getTestCertificate("ca1_root")); //$NON-NLS-1$ //$NON-NLS-2$
240 		} catch (Throwable t) {
241 			fail("Unexpected exception testing trusted self-signed cert: " + t.getMessage()); //$NON-NLS-1$
242 		}
243 	}
244 
245 	// findTrustAnchor positive test: chain with root trusted
testFindTrustAnchor1()246 	public void testFindTrustAnchor1() {
247 		try {
248 			Certificate cert = engine.findTrustAnchor(getTestCertificateChain("ca1_leafb", "ca1_ou", "ca1_root")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
249 			assertNotNull("Certificate did not come back in trusted root case", cert); //$NON-NLS-1$
250 			assertEquals("Output cert is not root trusted cert", cert, getTestCertificate("ca1_root")); //$NON-NLS-1$ //$NON-NLS-2$
251 		} catch (Throwable t) {
252 			fail("Unexpected exception testing trusted root from complete chain: " + t.getMessage()); //$NON-NLS-1$
253 		}
254 	}
255 
256 	// findTrustAnchor positive test: chain with intermediate trusted
testFindTrustAnchor2()257 	public void testFindTrustAnchor2() {
258 		try {
259 			Certificate cert = engine.findTrustAnchor(getTestCertificateChain("ca1_leafb", "ca1_ou", "ca1_root")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
260 			assertNotNull("Certificate did not come back in trusted intermediate case", cert); //$NON-NLS-1$
261 			assertEquals("Output cert is not intermediate trusted cert", cert, getTestCertificate("ca1_ou")); //$NON-NLS-1$ //$NON-NLS-2$
262 		} catch (Throwable t) {
263 			fail("Unexpected exception testing trusted root from complete chain: " + t.getMessage()); //$NON-NLS-1$
264 		}
265 	}
266 
267 	// findTrustAnchor positive test: chain with leaf trusted
testFindTrustAnchor3()268 	public void testFindTrustAnchor3() {
269 		try {
270 			Certificate cert = engine.findTrustAnchor(getTestCertificateChain("ca1_leafb", "ca1_ou", "ca1_root")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
271 			assertNotNull("Certificate did not come back in trusted leaf case", cert); //$NON-NLS-1$
272 			assertEquals("Output cert is not leaf trusted cert", cert, getTestCertificate("ca1_leafb")); //$NON-NLS-1$ //$NON-NLS-2$
273 		} catch (Throwable t) {
274 			fail("Unexpected exception testing trusted root from complete chain: " + t.getMessage()); //$NON-NLS-1$
275 		}
276 	}
277 
278 	// findTrustAnchor negative test: untrusted self signed
testFindTrustAnchor4()279 	public void testFindTrustAnchor4() {
280 		try {
281 			Certificate cert = engine.findTrustAnchor(new Certificate[] { getTestCertificate("ca2_root") }); //$NON-NLS-1$
282 			assertNull("Incorrectly returned a certificate for untrusted self-signed case", cert); //$NON-NLS-1$
283 		} catch (Throwable t) {
284 			fail("Unexpected exception testing untrusted self-signed cert: " + t.getMessage()); //$NON-NLS-1$
285 		}
286 	}
287 
288 	// findTrustAnchor negative test: untrusted chain
testFindTrustAnchor5()289 	public void testFindTrustAnchor5() {
290 		try {
291 			Certificate cert = engine.findTrustAnchor(getTestCertificateChain("ca2_leafb", "ca2_ou", "ca2_root")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
292 			assertNull("Incorrectly returned a certificate for untrusted chain case", cert); //$NON-NLS-1$
293 		} catch (Throwable t) {
294 			fail("Unexpected exception testing untrusted chain: " + t.getMessage()); //$NON-NLS-1$
295 		}
296 	}
297 
298 	// findTrustAnchor negative test: invalid chain
testFindTrustAnchor6()299 	public void testFindTrustAnchor6() {
300 		try {
301 			Certificate cert = engine.findTrustAnchor(getTestCertificateChain("ca2_leafa", "ca1_root")); //$NON-NLS-1$ //$NON-NLS-2$
302 			assertNull("Incorrectly returned a certificate on invalid certificate chain", cert); //$NON-NLS-1$
303 		} catch (Throwable t) {
304 			assertNull("Incorrectly thrown exception thrown on invalid certificate chain", t); //$NON-NLS-1$
305 		}
306 	}
307 
308 	// findTrustAnchor negative test: incomplete-able chain
testFindTrustAnchor7()309 	public void testFindTrustAnchor7() {
310 		try {
311 			Certificate cert = engine.findTrustAnchor(getTestCertificateChain("ca1_leafb", "ca1_root")); //$NON-NLS-1$ //$NON-NLS-2$
312 			assertNull("Incorrectly returned a certificate on incomplete-able certificate chain", cert); //$NON-NLS-1$
313 		} catch (Throwable t) {
314 			assertNull("Incorrectly thrown exception thrown on incomplete-able certificate chain", t); //$NON-NLS-1$
315 		}
316 	}
317 
318 	// findTrustAnchor negative test: null chain
testFindTrustAnchor8()319 	public void testFindTrustAnchor8() {
320 		try {
321 			engine.findTrustAnchor(null);
322 			fail("Did not throw IllegalArgumentException on NULL certificate"); //$NON-NLS-1$
323 		} catch (Throwable t) {
324 			assertTrue("Incorrect exception thrown on NULL certificate", t instanceof IllegalArgumentException); //$NON-NLS-1$
325 		}
326 	}
327 
328 	// testAddTrustAnchor positive test: add with alias
testAddTrustAnchor0()329 	public void testAddTrustAnchor0() {
330 		try {
331 			String alias = engine.addTrustAnchor(getTestCertificate("ca1_root"), "ca1_root"); //$NON-NLS-1$ //$NON-NLS-2$
332 			assertEquals("Alias returned does not equal alias input", alias, "ca1_root"); //$NON-NLS-1$ //$NON-NLS-2$
333 
334 		} catch (Throwable t) {
335 			fail("Unexpected exception adding trusted root: " + t.getMessage()); //$NON-NLS-1$
336 		}
337 	}
338 
339 	// testAddTrustAnchor positive test: add with autogenerated alias
testAddTrustAnchor1()340 	public void testAddTrustAnchor1() {
341 		try {
342 			String alias = engine.addTrustAnchor(getTestCertificate("ca1_root"), null); //$NON-NLS-1$
343 			assertNotNull("Generated alias was not correctly returned", alias); //$NON-NLS-1$
344 		} catch (Throwable t) {
345 			fail("Unexpected exception adding trusted root (autogen alias): " + t.getMessage()); //$NON-NLS-1$
346 		}
347 	}
348 
349 	// testAddTrustAnchor negative test: null cert specified
testAddTrustAnchor2()350 	public void testAddTrustAnchor2() {
351 		try {
352 			engine.addTrustAnchor(null, "ca1_root"); //$NON-NLS-1$
353 			fail("Did not throw IllegalArgumentException on NULL certificate"); //$NON-NLS-1$
354 		} catch (Throwable t) {
355 			assertTrue("Incorrect exception thrown on NULL certificate", t instanceof IllegalArgumentException); //$NON-NLS-1$
356 		}
357 	}
358 
359 	// testAddTrustAnchor negative test: existing cert specified
testAddTrustAnchor3()360 	public void testAddTrustAnchor3() {
361 		try {
362 			engine.addTrustAnchor(getTestCertificate("ca1_root"), "new_root"); //$NON-NLS-1$ //$NON-NLS-2$
363 			assertTrue("Did not throw CertificateException on duplicate cert", false); //$NON-NLS-1$
364 		} catch (Throwable t) {
365 			assertTrue("Incorrect exception thrown on duplicate cert", t instanceof CertificateException); //$NON-NLS-1$
366 			return;
367 		}
368 		fail("Expected exception when adding trust anchor"); //$NON-NLS-1$
369 	}
370 
371 	// testAddTrustAnchor negative test: existing alias specified
testAddTrustAnchor4()372 	public void testAddTrustAnchor4() {
373 		try {
374 			engine.addTrustAnchor(getTestCertificate("ca2_root"), "ca1_root"); //$NON-NLS-1$ //$NON-NLS-2$
375 			assertTrue("Did not throw CertificateException on duplicate alias", false); //$NON-NLS-1$
376 		} catch (Throwable t) {
377 			assertTrue("Incorrect exception thrown on duplicate alias", t instanceof CertificateException); //$NON-NLS-1$
378 			return;
379 		}
380 		fail("Expected exception when adding trust anchor"); //$NON-NLS-1$
381 	}
382 
383 	// removeTrustAnchor positive test: remove by alias
testRemoveTrustAnchor0()384 	public void testRemoveTrustAnchor0() {
385 		try {
386 			engine.removeTrustAnchor("ca1_root"); //$NON-NLS-1$
387 		} catch (Throwable t) {
388 			fail("Unexpected exception thrown when removing by alias: " + t.getMessage()); //$NON-NLS-1$
389 		}
390 	}
391 
392 	// removeTrustAnchor positive test: remove by cert
testRemoveTrustAnchor1()393 	public void testRemoveTrustAnchor1() {
394 		try {
395 			engine.removeTrustAnchor(getTestCertificate("ca1_root")); //$NON-NLS-1$
396 		} catch (Throwable t) {
397 			fail("Unexpected exception thrown when removing by cert: " + t.getMessage()); //$NON-NLS-1$
398 		}
399 	}
400 
401 	// removeTrustAnchor negative test: cert not found
testRemoveTrustAnchor2()402 	public void testRemoveTrustAnchor2() {
403 		try {
404 			engine.removeTrustAnchor(getTestCertificate("ca1_root")); //$NON-NLS-1$
405 			fail("Did not throw CertificateException on cert not found"); //$NON-NLS-1$
406 		} catch (Throwable t) {
407 			assertTrue("Incorrect exception thrown on remove by cert", t instanceof CertificateException); //$NON-NLS-1$
408 		}
409 	}
410 
411 	// removeTrustAnchor negative test: by alias not found
testRemoveTrustAnchor3()412 	public void testRemoveTrustAnchor3() {
413 		try {
414 			engine.removeTrustAnchor("ca2_root"); //$NON-NLS-1$
415 			assertTrue("Did not throw CertificateException on alias not found", false); //$NON-NLS-1$
416 		} catch (Throwable t) {
417 			assertTrue("Incorrect exception thrown on remove by alias", t instanceof CertificateException); //$NON-NLS-1$
418 			return;
419 		}
420 		fail("Expected exception when removing trust anchor"); //$NON-NLS-1$
421 	}
422 
423 	// removeTrustAnchor negative test: remove by null alias
testRemoveTrustAnchor4()424 	public void testRemoveTrustAnchor4() {
425 		try {
426 			engine.removeTrustAnchor((String) null);
427 			fail("Did not throw CertificateException on alias null"); //$NON-NLS-1$
428 		} catch (Throwable t) {
429 			assertTrue("Incorrect exception thrown on remove by null alias", t instanceof IllegalArgumentException); //$NON-NLS-1$
430 		}
431 	}
432 
433 	// removeTrustAnchor negative test: remove by null certificate
testRemoveTrustAnchor5()434 	public void testRemoveTrustAnchor5() {
435 		try {
436 			engine.removeTrustAnchor((Certificate) null);
437 			fail("Did not throw IllegalArgumentException on remove by cert null"); //$NON-NLS-1$
438 		} catch (Throwable t) {
439 			assertTrue("Incorrect exception thrown on remove by null cert", t instanceof IllegalArgumentException); //$NON-NLS-1$
440 		}
441 	}
442 
443 	// getTrustAnchor positive test: get by alias
testGetTrustAnchor0()444 	public void testGetTrustAnchor0() {
445 		try {
446 			Certificate cert = engine.getTrustAnchor("ca1_root"); //$NON-NLS-1$
447 			assertEquals("Did not get expected certificate", getTestCertificate("ca1_root"), cert); //$NON-NLS-1$ //$NON-NLS-2$
448 		} catch (Throwable t) {
449 			fail("Unexpected exception when retrieving trust anchor: " + t.getMessage()); //$NON-NLS-1$
450 		}
451 	}
452 
453 	// getTrustAnchor negative test: get by null alias
testGetTrustAnchor1()454 	public void testGetTrustAnchor1() {
455 		try {
456 			engine.getTrustAnchor(null);
457 			fail("Did not throw IllegalArgumentException on get by alias null"); //$NON-NLS-1$
458 		} catch (Throwable t) {
459 			assertTrue("Incorrect exception thrown on remove by null alias", t instanceof IllegalArgumentException); //$NON-NLS-1$
460 		}
461 	}
462 
463 	// getTrustAnchor negative test: does not exist
testGetTrustAnchor2()464 	public void testGetTrustAnchor2() {
465 		try {
466 			Certificate cert = engine.getTrustAnchor("ca2_root"); //$NON-NLS-1$
467 			assertNull("Incorrectly returned a certificate on certificate does not exist", cert); //$NON-NLS-1$
468 		} catch (Throwable t) {
469 			assertNull("Incorrectly thrown exception on alias does not exist", t); //$NON-NLS-1$
470 			return;
471 		}
472 	}
473 
474 	// getAliases positive test: get the alias list
testGetAliases0()475 	public void testGetAliases0() {
476 		try {
477 			engine.getAliases();
478 		} catch (Throwable t) {
479 			fail("Unexpected exception when retrieving alias list: " + t.getMessage()); //$NON-NLS-1$
480 		}
481 
482 	}
483 	// TODO: thread safety tests
484 	// TODO: performance tests
485 }
486