diff --git a/tests/unit/helpers/beautify.spec.ts b/tests/unit/helpers/beautify.spec.ts new file mode 100644 index 00000000..f19920f4 --- /dev/null +++ b/tests/unit/helpers/beautify.spec.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest' + +import { beautify } from '../../../src/helpers/beautify' + +describe.concurrent('helpers/elasticsearchAdapter.ts', () => { + describe.concurrent('beautify.ts beautify', () => { + it('should return empty string when supplied with empty string', () => { + expect(beautify('')).toBe('') + }) + + it('should return undefined when supplied with undefined', () => { + expect(beautify(undefined as unknown as string)).toBe(undefined) + }) + + it('should return null when supplied with null', () => { + expect(beautify(null as unknown as string)).toBe(null) + }) + + it('should return false when supplied with false', () => { + expect(beautify(false as unknown as string)).toBe(false) + }) + + it('should return 0 when supplied with 0', () => { + expect(beautify(0 as unknown as string)).toBe(0) + }) + + it('should return empty string when supplied with empty string', () => { + const text = '{"a":123456789012345678901234567890}' + + const normalJSONStrinigified = JSON.stringify(JSON.parse(text)) + + expect(normalJSONStrinigified).toBe('{"a":1.2345678901234568e+29}') + + expect(beautify(text)).toBe('{\n "a": 1.2345678901234567890123456789e+29\n}') + }) + + it('should return the input string if not parseable as JSON', () => { + const text = 'this { is not } json' + + expect(beautify(text)).toBe(text) + }) + }) +}) \ No newline at end of file diff --git a/tests/unit/helpers/elasticsearchAdapter.spec.ts b/tests/unit/helpers/elasticsearchAdapter.spec.ts new file mode 100644 index 00000000..2eaa712c --- /dev/null +++ b/tests/unit/helpers/elasticsearchAdapter.spec.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest' + +import { buildFetchAuthHeader, addTrailingSlash, uriWithCredentials } from '../../../src/helpers/elasticsearchAdapter' + +describe.concurrent('helpers/elasticsearchAdapter.ts', () => { + describe.concurrent('elasticsearchAdapter.ts buildFetchAuthHeader', () => { + it('should base64-encode username and password when both are non-zero length', () => { + const result = buildFetchAuthHeader('username', 'password') + + const [schema, encoded] = result.split(' ') + + expect(schema).toBe('Basic') + expect(decodeURIComponent(escape(atob(encoded))).split(':')).toEqual(['username', 'password']) + }) + + it('should base64-encode only username when non-zero length and password is zero-length', () => { + const result = buildFetchAuthHeader('username', '') + + const [schema, encoded] = result.split(' ') + + expect(schema).toBe('Basic') + expect(decodeURIComponent(escape(atob(encoded)))).toEqual('username') + }) + + it('should return ApiKey schema and plaintext password when username is zero-length and password is non-zero length', () => { + const result = buildFetchAuthHeader('', 'password') + + const [schema, apiKey] = result.split(' ') + + expect(schema).toBe('ApiKey') + expect(apiKey).toBe('password') + }) + + it('should return empty string when both username and password are zero-length', () => { + const result = buildFetchAuthHeader('', '') + + expect(result).toBe('') + }) + }) + + describe.concurrent('elasticsearchAdapter.ts addTrailingSlash', () => { + it('should not append a / if uri already ends with a /', () => { + const uri = 'http://localhost:9200/' + const result = addTrailingSlash(uri) + expect(result).toBe(uri) + }) + + it('should append a / if uri does not end with a /', () => { + const uri = 'http://localhost:9200' + const result = addTrailingSlash(uri) + expect(result).toBe(uri + '/') + }) + }) + + describe.concurrent('elasticsearchAdapter.ts uriWithCredentials', () => { + it('should stringify URI with username and password when username is non-zero length', () => { + const uri = 'http://localhost:9200/' + const result = uriWithCredentials(uri, 'username', 'password') + expect(result).toBe('http://username:password@localhost:9200/') + }) + + it('should not stringify URI with username and password when username is zero length', () => { + const uri = 'http://localhost:9200/' + const result = uriWithCredentials(uri, '', 'password') + expect(result).toBe(uri) + }) + + it('should replace embedded username and password when new username is non-zero length', () => { + const uri = 'http://original_username:original_password@localhost:9200/' + const result = uriWithCredentials(uri, 'username', 'password') + expect(result).toBe('http://username:password@localhost:9200/') + }) + + it('should replace username and retain password when new username is zero length', () => { + const uri = 'http://original_username:original_password@localhost:9200/' + const result = uriWithCredentials(uri, '', 'password') + expect(result).toBe('http://:original_password@localhost:9200/') + }) + }) +}) \ No newline at end of file diff --git a/tests/unit/helpers/flatten.spec.ts b/tests/unit/helpers/flatten.spec.ts new file mode 100644 index 00000000..d96db89c --- /dev/null +++ b/tests/unit/helpers/flatten.spec.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest' + +import { flattenObj } from '../../../src/helpers/flatten' + +describe.concurrent('helpers/flatten.ts', () => { + describe.concurrent('flatten.ts flattenObj', () => { + it('should flatten all root objects to dot notation', () => { + const obj = { + a: 1, + b: { + c: { + d: 2, + } + }, + e: [3, 4, { f: { g: 5 } }], + h: { + i: 6 + } + } + + expect(flattenObj(obj)).toStrictEqual({ + a: 1, + 'b.c.d': 2, + e: [3, 4, { f: { g: 5 } }], + 'h.i': 6 + }) + }) + + it('should throw a RangeError when input object contains a circular reference', () => { + const obj = { + a: 1 + } + + obj['b'] = obj + + expect(() => flattenObj(obj)).toThrowError(RangeError) + }) + + it('should flatten null to an empty object', () => { + expect(flattenObj(null)).toStrictEqual({}) + }) + + it('should flatten undefined to an empty object', () => { + expect(flattenObj(undefined)).toStrictEqual({}) + }) + + it('should flatten number to an empty object', () => { + expect(flattenObj(17)).toStrictEqual({}) + }) + + it('should flatten boolean to an empty object', () => { + expect(flattenObj(true)).toStrictEqual({}) + }) + + it('should flatten array to an index+character object', () => { + expect(flattenObj(['a', 'b'])).toStrictEqual({ + 0: 'a', + 1: 'b' + }) + }) + + it('should flatten string to index+character object', () => { + expect(flattenObj('asdf')).toStrictEqual({ + 0: 'a', + 1: 's', + 2: 'd', + 3: 'f' + }) + }) + }) +}) \ No newline at end of file diff --git a/tests/unit/helpers/newCluster.spec.ts b/tests/unit/helpers/newCluster.spec.ts new file mode 100644 index 00000000..718c71d1 --- /dev/null +++ b/tests/unit/helpers/newCluster.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest' + +import { newElasticsearchCluster } from '../../../src/helpers/newCluster' +import { DEFAULT_CLUSTER_NAME, DEFAULT_CLUSTER_URI } from '../../../src/consts.ts' + +describe.concurrent('helpers/newCluster.ts', () => { + describe.concurrent('newCluster.ts newElasticsearchCluster', () => { + it('should return empty string when supplied with empty string', () => { + expect(newElasticsearchCluster()).toEqual({ + name: DEFAULT_CLUSTER_NAME, + distribution: '', + username: '', + password: '', + uri: DEFAULT_CLUSTER_URI, + clusterName: '', + version: '', + majorVersion: '', + uuid: '', + status: '' + }) + }) + }) +}) \ No newline at end of file diff --git a/tests/unit/helpers/nodes.spec.ts b/tests/unit/helpers/nodes.spec.ts new file mode 100644 index 00000000..10ada415 --- /dev/null +++ b/tests/unit/helpers/nodes.spec.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest' + +import { nodeRoleTitle } from '../../../src/helpers/nodes' + +const NEWLINE = '\r\n' + +describe.concurrent('helpers/nodes.ts', () => { + describe.concurrent('nodes.ts nodeRoleTitle', () => { + it('should return empty string when supplied with empty string', () => { + expect(nodeRoleTitle('')).toBe('') + }) + + it('should return all role titles when input string contains them all', () => { + // include all lowercase latin alphabet characters and hyphen character for future proofing + const nodes = new Set(nodeRoleTitle('abcdefghijklmnopqrstuvwxyz-').trimEnd().split(NEWLINE)) + + expect(nodes.size).toBe(13) + + expect(nodes).toContain('c - cold node') + expect(nodes).toContain('d - data node') + expect(nodes).toContain('f - frozen node') + expect(nodes).toContain('h - hot node') + expect(nodes).toContain('i - ingest node') + expect(nodes).toContain('l - machine learning node') + expect(nodes).toContain('m - master-eligible node') + expect(nodes).toContain('r - remote cluster client node') + expect(nodes).toContain('s - content node') + expect(nodes).toContain('t - transform node') + expect(nodes).toContain('v - voting-only node') + expect(nodes).toContain('w - warm node') + expect(nodes).toContain('coordinating nodes') + }) + + it('should only include a single role title when input is a single role character', () => { + const supportedRoles = new Map([ + ['c','c - cold node'], + ['d','d - data node'], + ['f','f - frozen node'], + ['h','h - hot node'], + ['i','i - ingest node'], + ['l','l - machine learning node'], + ['m','m - master-eligible node'], + ['r','r - remote cluster client node'], + ['s','s - content node'], + ['t','t - transform node'], + ['v','v - voting-only node'], + ['w','w - warm node'], + ['-','coordinating nodes'] + ]) + + for (const [role, title] of supportedRoles) { + expect(nodeRoleTitle(role).trimEnd()).toBe(title) + } + }) + + it('should return an empty string for all unsupported latin-1 lowercase letters', () => { + const unsupportedRoleTypes = ['a', 'b', 'e', 'g', 'j', 'k', 'n', 'o', 'p', 'q', 'u', 'x', 'y', 'z'] + + for (const role of unsupportedRoleTypes) { + expect(nodeRoleTitle(role)).toBe('') + } + }) + }) +}) \ No newline at end of file diff --git a/tests/unit/helpers/search.spec.ts b/tests/unit/helpers/search.spec.ts new file mode 100644 index 00000000..73daa371 --- /dev/null +++ b/tests/unit/helpers/search.spec.ts @@ -0,0 +1,88 @@ +import { describe, it, expect } from 'vitest' + +import { sortableField } from '../../../src/helpers/search' +import { DEFAULT_SORTABLE_COLUMNS } from '../../../src/consts' + +describe.concurrent('helpers/search.ts', () => { + describe.concurrent('search.ts sortableField', () => { + it('should return column name for all default sortable columns', () => { + for (const column of DEFAULT_SORTABLE_COLUMNS) { + expect(sortableField(column, null)).toBe(column) + } + }) + + it('should return null when fieldName is not a default sortable column name and property is falsy', () => { + for (const falsyProperty of [null, undefined, 0, '', false, NaN]) { + expect(sortableField('non-default sortable column', falsyProperty)).toBe(null) + } + }) + + it('should return fieldName when property.type is a sortable type', () => { + for (const type of ['long', 'integer', 'double', 'float', 'date', 'boolean', 'keyword']) { + const property = { + type: 'keyword' + } + + expect(sortableField('random fieldName', property)).toBe('random fieldName') + } + }) + + it('should return fieldName with \'.keyword\' appended when property.fields.keyword is truthy', () => { + const property = { + fields: { + keyword: 'some value' + } + } + + expect(sortableField('random fieldName', property)).toBe('random fieldName.keyword') + }) + + it('should return fieldName with \'.keyword\' appended when property.fields.keyword is truthy', () => { + const property = { + fields: { + keyword: 'some value' + } + } + + expect(sortableField('random fieldName', property)).toBe('random fieldName.keyword') + }) + + it('should return fieldName appended with subfield type when subfield type is sortable', () => { + for (const type of ['long', 'integer', 'double', 'float', 'date', 'boolean', 'keyword']) { + const property = { + fields: { + a: { + type + }, + b: 2 + } + } + + expect(sortableField('random fieldName', property)).toBe('random fieldName.a') + } + }) + + it('should return null when subfield type is non-sortable', () => { + const property = { + fields: { + a: { + type: 'non-sortable type' + } + } + } + + expect(sortableField('random fieldName', property)).toBe(null) + }) + + it('should return property has fields but none of them have a type', () => { + const property = { + fields: { + a: {}, + b: {} + } + } + + expect(sortableField('random fieldName', property)).toBe(null) + }) + }) +}) \ No newline at end of file diff --git a/tests/unit/helpers/tableColumns.spec.ts b/tests/unit/helpers/tableColumns.spec.ts new file mode 100644 index 00000000..8703bf2d --- /dev/null +++ b/tests/unit/helpers/tableColumns.spec.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest' + +import { genColumns } from '../../../src/helpers/tableColumns' + +describe.concurrent('helpers/tableColumns.ts', () => { + describe.concurrent('tableColumns.ts genColumns', () => { + it('should return empty array when supplied with an empty array', () => { + expect(genColumns([])).toStrictEqual([]) + }) + + it('should map all options in the input array', () => { + const options = [ + { + label: 'label value 1', + field: 'field value 1', + align: 'align value 1' + }, + { + label: 'label value 2', + field: 'field value 2', + align: 'align value 2' + } + ] + + expect(genColumns(options)).toStrictEqual([ + { + label: 'label value 1', + field: 'field value 1', + name: 'field value 1', + sortable: true, + align: 'align value 1' + }, + { + label: 'label value 2', + field: 'field value 2', + name: 'field value 2', + sortable: true, + align: 'align value 2' + } + ]) + }) + + it('should set sortable to false when field is undefined or an empty string', () => { + const options = [ + { + label: 'label value 1', + field: undefined, + align: 'align value 1' + }, + { + label: 'label value 2', + field: '', + align: 'align value 2' + } + ] + + expect(genColumns(options)).toStrictEqual([ + expect.objectContaining({ + field: undefined, + name: undefined, + sortable: false + }), + expect.objectContaining({ + field: '', + name: '', + sortable: false + }) + ]) + }) + + it('should set align to \'left\' when undefind or an empty string', () => { + const options = [ + { + label: 'label value 1', + field: 'field value 1', + align: undefined + }, + { + label: 'label value 2', + field: 'field value 2', + align: '' + } + ] + + expect(genColumns(options)).toStrictEqual([ + expect.objectContaining({ + align: 'left' + }), + expect.objectContaining({ + align: 'left' + }) + ]) + }) + }) +}) \ No newline at end of file