diff --git a/src/servers/data/index.ts b/src/servers/data/index.ts index 32c8a75a..9d590288 100644 --- a/src/servers/data/index.ts +++ b/src/servers/data/index.ts @@ -1,3 +1,5 @@ +import { SemVer } from '../../utils/helpers/version'; + export interface ServerData { name: string; url: string; @@ -9,7 +11,7 @@ export interface ServerWithId extends ServerData { } export interface ReachableServer extends ServerWithId { - version: string; + version: SemVer; printableVersion: string; } diff --git a/src/utils/helpers/version.ts b/src/utils/helpers/version.ts index a44a0251..e3695eb1 100644 --- a/src/utils/helpers/version.ts +++ b/src/utils/helpers/version.ts @@ -2,12 +2,20 @@ import { compare } from 'compare-versions'; import { identity, memoizeWith } from 'ramda'; import { Empty, hasValue } from '../utils'; +type SemVerPatternFragment = `${bigint | '*'}`; + +export type SemVerPattern = SemVerPatternFragment +| `${SemVerPatternFragment}.${SemVerPatternFragment}` +| `${SemVerPatternFragment}.${SemVerPatternFragment}.${SemVerPatternFragment}`; + export interface Versions { - maxVersion?: string; - minVersion?: string; + maxVersion?: SemVerPattern; + minVersion?: SemVerPattern; } -export const versionMatch = (versionToMatch: string | Empty, { maxVersion, minVersion }: Versions): boolean => { +export type SemVer = `${bigint}.${bigint}.${bigint}`; + +export const versionMatch = (versionToMatch: SemVer | Empty, { maxVersion, minVersion }: Versions): boolean => { if (!hasValue(versionToMatch)) { return false; } @@ -18,7 +26,7 @@ export const versionMatch = (versionToMatch: string | Empty, { maxVersion, minVe return matchesMaxVersion && matchesMinVersion; }; -const versionIsValidSemVer = memoizeWith(identity, (version: string) => { +const versionIsValidSemVer = memoizeWith(identity, (version: string): version is SemVerPattern => { try { return compare(version, version, '='); } catch (e) { diff --git a/test/common/MenuLayout.test.tsx b/test/common/MenuLayout.test.tsx index 85cbc109..1e7219b5 100644 --- a/test/common/MenuLayout.test.tsx +++ b/test/common/MenuLayout.test.tsx @@ -6,6 +6,7 @@ import { Mock } from 'ts-mockery'; import createMenuLayout from '../../src/common/MenuLayout'; import { NonReachableServer, NotFoundServer, ReachableServer, SelectedServer } from '../../src/servers/data'; import NoMenuLayout from '../../src/common/NoMenuLayout'; +import { SemVer } from '../../src/utils/helpers/version'; describe('', () => { const ServerError = jest.fn(); @@ -48,11 +49,11 @@ describe('', () => { }); it.each([ - [ '2.1.0', 6 ], - [ '2.2.0', 7 ], - [ '2.5.0', 7 ], - [ '2.6.0', 8 ], - [ '2.7.0', 8 ], + [ '2.1.0' as SemVer, 6 ], + [ '2.2.0' as SemVer, 7 ], + [ '2.5.0' as SemVer, 7 ], + [ '2.6.0' as SemVer, 8 ], + [ '2.7.0' as SemVer, 8 ], ])('has expected amount of routes based on selected server\'s version', (version, expectedAmountOfRoutes) => { const selectedServer = Mock.of({ version }); const wrapper = createWrapper(selectedServer).dive(); diff --git a/test/common/ShlinkVersions.test.tsx b/test/common/ShlinkVersions.test.tsx index 0a97010f..03af9357 100644 --- a/test/common/ShlinkVersions.test.tsx +++ b/test/common/ShlinkVersions.test.tsx @@ -14,11 +14,11 @@ describe('', () => { afterEach(() => wrapper?.unmount()); it.each([ - [ '1.2.3', Mock.of({ version: '', printableVersion: 'foo' }), 'v1.2.3', 'foo' ], - [ 'foo', Mock.of({ version: '', printableVersion: '1.2.3' }), 'latest', '1.2.3' ], - [ 'latest', Mock.of({ version: '', printableVersion: 'latest' }), 'latest', 'latest' ], - [ '5.5.0', Mock.of({ version: '', printableVersion: '0.2.8' }), 'v5.5.0', '0.2.8' ], - [ 'not-semver', Mock.of({ version: '', printableVersion: 'something' }), 'latest', 'something' ], + [ '1.2.3', Mock.of({ version: '1.0.0', printableVersion: 'foo' }), 'v1.2.3', 'foo' ], + [ 'foo', Mock.of({ version: '1.0.0', printableVersion: '1.2.3' }), 'latest', '1.2.3' ], + [ 'latest', Mock.of({ version: '1.0.0', printableVersion: 'latest' }), 'latest', 'latest' ], + [ '5.5.0', Mock.of({ version: '1.0.0', printableVersion: '0.2.8' }), 'v5.5.0', '0.2.8' ], + [ 'not-semver', Mock.of({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some' ], ])( 'displays expected versions when selected server is reachable', (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { diff --git a/test/servers/helpers/ForServerVersion.test.tsx b/test/servers/helpers/ForServerVersion.test.tsx index 4aabf23f..2167b3d6 100644 --- a/test/servers/helpers/ForServerVersion.test.tsx +++ b/test/servers/helpers/ForServerVersion.test.tsx @@ -2,11 +2,12 @@ import { mount, ReactWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; import ForServerVersion from '../../../src/servers/helpers/ForServerVersion'; import { ReachableServer, SelectedServer } from '../../../src/servers/data'; +import { SemVer, SemVerPattern } from '../../../src/utils/helpers/version'; describe('', () => { let wrapped: ReactWrapper; - const renderComponent = (selectedServer: SelectedServer, minVersion?: string, maxVersion?: string) => { + const renderComponent = (selectedServer: SelectedServer, minVersion?: SemVerPattern, maxVersion?: SemVerPattern) => { wrapped = mount( Hello @@ -19,15 +20,15 @@ describe('', () => { afterEach(() => wrapped?.unmount()); it('does not render children when current server is empty', () => { - const wrapped = renderComponent(null, '1'); + const wrapped = renderComponent(null, '1.*.*'); expect(wrapped.html()).toBeNull(); }); it.each([ - [ '2.0.0', undefined, '1.8.3' ], - [ undefined, '1.8.0', '1.8.3' ], - [ '1.7.0', '1.8.0', '1.8.3' ], + [ '2.0.0' as SemVer, undefined, '1.8.3' as SemVer ], + [ undefined, '1.8.0' as SemVer, '1.8.3' as SemVer ], + [ '1.7.0' as SemVer, '1.8.0' as SemVer, '1.8.3' as SemVer ], ])('does not render children when current version does not match requirements', (min, max, version) => { const wrapped = renderComponent(Mock.of({ version, printableVersion: version }), min, max); @@ -35,11 +36,11 @@ describe('', () => { }); it.each([ - [ '2.0.0', undefined, '2.8.3' ], - [ '2.0.0', undefined, '2.0.0' ], - [ undefined, '1.8.0', '1.8.0' ], - [ undefined, '1.8.0', '1.7.1' ], - [ '1.7.0', '1.8.0', '1.7.3' ], + [ '2.0.0' as SemVer, undefined, '2.8.3' as SemVer ], + [ '2.0.0' as SemVer, undefined, '2.0.0' as SemVer ], + [ undefined, '1.8.0' as SemVer, '1.8.0' as SemVer ], + [ undefined, '1.8.0' as SemVer, '1.7.1' as SemVer ], + [ '1.7.0' as SemVer, '1.8.0' as SemVer, '1.7.3' as SemVer ], ])('renders children when current version matches requirements', (min, max, version) => { const wrapped = renderComponent(Mock.of({ version, printableVersion: version }), min, max); diff --git a/test/short-urls/ShortUrlsTable.test.tsx b/test/short-urls/ShortUrlsTable.test.tsx index 74a736b3..7164e40e 100644 --- a/test/short-urls/ShortUrlsTable.test.tsx +++ b/test/short-urls/ShortUrlsTable.test.tsx @@ -5,6 +5,7 @@ import { ShortUrlsTable as shortUrlsTableCreator } from '../../src/short-urls/Sh import { OrderableFields, SORTABLE_FIELDS } from '../../src/short-urls/reducers/shortUrlsListParams'; import { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList'; import { ReachableServer, SelectedServer } from '../../src/servers/data'; +import { SemVer } from '../../src/utils/helpers/version'; describe('', () => { let wrapper: ShallowWrapper; @@ -61,10 +62,10 @@ describe('', () => { }); it.each([ - [ '2.6.0' ], - [ '2.6.1' ], - [ '2.7.0' ], - [ '3.0.0' ], + [ '2.6.0' as SemVer ], + [ '2.6.1' as SemVer ], + [ '2.7.0' as SemVer ], + [ '3.0.0' as SemVer ], ])('should render composed column when server supports title', (version) => { const wrapper = createWrapper(Mock.of({ version })); const composedColumn = wrapper.find('table').find('th').at(2); diff --git a/test/short-urls/helpers/QrCodeModal.test.tsx b/test/short-urls/helpers/QrCodeModal.test.tsx index d84b5572..b57b8fcc 100644 --- a/test/short-urls/helpers/QrCodeModal.test.tsx +++ b/test/short-urls/helpers/QrCodeModal.test.tsx @@ -7,11 +7,12 @@ import { ShortUrl } from '../../../src/short-urls/data'; import { ReachableServer } from '../../../src/servers/data'; import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon'; import { DropdownBtn } from '../../../src/utils/DropdownBtn'; +import { SemVer } from '../../../src/utils/helpers/version'; describe('', () => { let wrapper: ShallowWrapper; const shortUrl = 'https://doma.in/abc123'; - const createWrapper = (version = '2.6.0') => { + const createWrapper = (version: SemVer = '2.6.0') => { const selectedServer = Mock.of({ version }); wrapper = shallow( @@ -37,12 +38,12 @@ describe('', () => { }); it.each([ - [ '2.3.0', 0, '/qr-code/300' ], - [ '2.4.0', 0, '/qr-code/300?format=png' ], - [ '2.4.0', 10, '/qr-code/300?format=png' ], - [ '2.5.0', 0, '/qr-code?size=300&format=png' ], - [ '2.6.0', 0, '/qr-code?size=300&format=png' ], - [ '2.6.0', 10, '/qr-code?size=300&format=png&margin=10' ], + [ '2.3.0' as SemVer, 0, '/qr-code/300' ], + [ '2.4.0' as SemVer, 0, '/qr-code/300?format=png' ], + [ '2.4.0' as SemVer, 10, '/qr-code/300?format=png' ], + [ '2.5.0' as SemVer, 0, '/qr-code?size=300&format=png' ], + [ '2.6.0' as SemVer, 0, '/qr-code?size=300&format=png' ], + [ '2.6.0' as SemVer, 10, '/qr-code?size=300&format=png&margin=10' ], ])('displays an image with the QR code of the URL', (version, margin, expectedUrl) => { const wrapper = createWrapper(version); const formControls = wrapper.find('.form-control-range'); @@ -84,9 +85,9 @@ describe('', () => { }); it.each([ - [ '2.3.0', 0, 'col-12' ], - [ '2.4.0', 1, 'col-md-6' ], - [ '2.6.0', 1, 'col-md-4' ], + [ '2.3.0' as SemVer, 0, 'col-12' ], + [ '2.4.0' as SemVer, 1, 'col-md-6' ], + [ '2.6.0' as SemVer, 1, 'col-md-4' ], ])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => { const wrapper = createWrapper(version); const dropdown = wrapper.find(DropdownBtn);