From 134e0df441e6853ed29d448baf68d89feeab8b34 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 16 Jan 2024 13:21:28 -0500 Subject: [PATCH] Add a `toHavePropertiesOf` custom Jasmine matcher. --- .eslintrc.json | 1 + src/shadowbox/server/manager_service.spec.ts | 10 +++-- src/shadowbox/server/testing/matchers.ts | 47 ++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/shadowbox/server/testing/matchers.ts diff --git a/.eslintrc.json b/.eslintrc.json index 5f7d214f..637f4560 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -73,6 +73,7 @@ "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-namespace": ["error", {"allowDeclarations": true}], "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-unused-vars": [ "warn", diff --git a/src/shadowbox/server/manager_service.spec.ts b/src/shadowbox/server/manager_service.spec.ts index c5023c4c..942cbefb 100644 --- a/src/shadowbox/server/manager_service.spec.ts +++ b/src/shadowbox/server/manager_service.spec.ts @@ -24,6 +24,7 @@ import {FakePrometheusClient, FakeShadowsocksServer} from './mocks/mocks'; import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key'; import {ServerConfigJson} from './server_config'; import {SharedMetricsPublisher} from './shared_metrics'; +import {customMatchers} from './testing/matchers'; interface ServerInfo { name: string; @@ -50,6 +51,7 @@ describe('ShadowsocksManagerService', () => { // callback is invoked, followed by the next (done) callback. let responseProcessed = false; beforeEach(() => { + jasmine.addMatchers(customMatchers); responseProcessed = false; }); afterEach(() => { @@ -296,8 +298,8 @@ describe('ShadowsocksManagerService', () => { expect(data.accessKeys.length).toEqual(2); const serviceAccessKey1 = data.accessKeys[0]; const serviceAccessKey2 = data.accessKeys[1]; - expect(Object.keys(serviceAccessKey1).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); - expect(Object.keys(serviceAccessKey2).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); + expect(serviceAccessKey1).toHavePropertiesOf(EXPECTED_ACCESS_KEY_PROPERTIES); + expect(serviceAccessKey2).toHavePropertiesOf(EXPECTED_ACCESS_KEY_PROPERTIES); expect(serviceAccessKey1.name).toEqual(accessKeyName); responseProcessed = true; // required for afterEach to pass. }, @@ -385,7 +387,7 @@ describe('ShadowsocksManagerService', () => { const res = { send: (httpCode, data) => { expect(httpCode).toEqual(201); - expect(Object.keys(data).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); + expect(data).toHavePropertiesOf(EXPECTED_ACCESS_KEY_PROPERTIES); expect(data.method).toEqual('chacha20-ietf-poly1305'); responseProcessed = true; // required for afterEach to pass. }, @@ -397,7 +399,7 @@ describe('ShadowsocksManagerService', () => { const res = { send: (httpCode, data) => { expect(httpCode).toEqual(201); - expect(Object.keys(data).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); + expect(data).toHavePropertiesOf(EXPECTED_ACCESS_KEY_PROPERTIES); expect(data.method).toEqual('aes-256-gcm'); responseProcessed = true; // required for afterEach to pass. }, diff --git a/src/shadowbox/server/testing/matchers.ts b/src/shadowbox/server/testing/matchers.ts new file mode 100644 index 00000000..0df91504 --- /dev/null +++ b/src/shadowbox/server/testing/matchers.ts @@ -0,0 +1,47 @@ +// Copyright 2024 The Outline 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. + +/** + * Matchers defined in this module must be declared in this interface for + * TypeScript to be happy. + */ +declare global { + namespace jasmine { + interface Matchers { + toHavePropertiesOf(expected: string[]): void; + } + } +} + +/** + * Custom Jasmine matchers. + */ +export const customMatchers: jasmine.CustomMatcherFactories = { + // Compare two objects and returns true if actual contains all the + // properties in the expected. + toHavePropertiesOf: (util: jasmine.MatchersUtil): jasmine.CustomMatcher => { + return { + compare: (actual: Object, expected: string[]): jasmine.CustomMatcherResult => { + const actualProperties = Object.keys(actual); + const isEqual = util.equals(expected.sort(), actualProperties.sort()); + return { + pass: isEqual, + message: + `Expected ${jasmine.pp(actual)}${isEqual ? '' : ' not'} ` + + `to contain properties in ${jasmine.pp(expected)}`, + }; + }, + }; + }, +};