diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e546260f..b94d351b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] +### Added +* [#1360](https://github.com/shlinkio/shlink-web-client/issues/1360) Added ability for server IDs to be generated based on the server name and URL, instead of generating a random UUID. + + This can improve sharing a predefined set of servers cia servers.json, env vars, or simply export and import your servers in some other device, and then be able to share server URLs which continue working. + + All existing servers will keep their generated IDs in existing devices for backwards compatibility, but newly created servers will use the new approach. + +### Changed +* *Nothing* + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + + ## [4.2.2] - 2024-10-19 ### Added * *Nothing* diff --git a/README.md b/README.md index e2060830d..11610708c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![GitHub license](https://img.shields.io/github/license/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE) [![Mastodon](https://img.shields.io/mastodon/follow/109329425426175098?color=%236364ff&domain=https%3A%2F%2Ffosstodon.org&label=follow&logo=mastodon&logoColor=white&style=flat-square)](https://fosstodon.org/@shlinkio) -[![Bluesky](https://img.shields.io/badge/follow-shlinkio-0285FF.svg?style=flat-square&logo=bluesky&logoColor=white)](https://bsky.app/profile/shlinkio.bsky.social) +[![Bluesky](https://img.shields.io/badge/follow-shlinkio-0285FF.svg?style=flat-square&logo=bluesky&logoColor=white)](https://bsky.app/profile/shlink.io) [![Paypal Donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=cccccc)](https://slnk.to/donate) A ReactJS-based progressive web application for [Shlink](https://shlink.io). diff --git a/package-lock.json b/package-lock.json index 2e7f0c22f..1ba44e04a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,6 @@ "react-router-dom": "^6.27.0", "reactstrap": "^9.2.3", "redux-localstorage-simple": "^2.5.1", - "uuid": "^10.0.0", "workbox-core": "^7.1.0", "workbox-expiration": "^7.1.0", "workbox-precaching": "^7.1.0", @@ -11580,19 +11579,6 @@ "dev": true, "license": "MIT" }, - "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -20190,11 +20176,6 @@ "version": "1.0.2", "dev": true }, - "uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" - }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 080a9f546..65bee6d10 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "react-router-dom": "^6.27.0", "reactstrap": "^9.2.3", "redux-localstorage-simple": "^2.5.1", - "uuid": "^10.0.0", "workbox-core": "^7.1.0", "workbox-expiration": "^7.1.0", "workbox-precaching": "^7.1.0", diff --git a/scripts/docker/servers_from_env.sh b/scripts/docker/servers_from_env.sh index 4275f5911..8c49f674d 100755 --- a/scripts/docker/servers_from_env.sh +++ b/scripts/docker/servers_from_env.sh @@ -4,6 +4,8 @@ set -e ME=$(basename $0) +# In order to allow people to pre-configure a server in their shlink-web-client instance via env vars, this function +# dumps a servers.json file based on the values provided via env vars setup_single_shlink_server() { [ -n "$SHLINK_SERVER_URL" ] || return 0 [ -n "$SHLINK_SERVER_API_KEY" ] || return 0 diff --git a/src/app/App.tsx b/src/app/App.tsx index 3c7c540d5..aa86063e8 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -50,8 +50,8 @@ const App: FCWithDeps = ( const isHome = location.pathname === '/'; useEffect(() => { - // Try to fetch the remote servers if the list is empty at first - // We use a ref because we don't care if the servers list becomes empty later + // Try to fetch the remote servers if the list is empty during first render. + // We use a ref because we don't care if the servers list becomes empty later. if (Object.keys(initialServers.current).length === 0) { fetchServers(); } diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index 6744e1275..c40cc6d09 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -8,8 +8,8 @@ import { NoMenuLayout } from '../common/NoMenuLayout'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import { useGoBack } from '../utils/helpers/hooks'; -import { randomUUID } from '../utils/utils'; import type { ServerData, ServersMap, ServerWithId } from './data'; +import { ensureUniqueIds } from './helpers'; import { DuplicatedServersModal } from './helpers/DuplicatedServersModal'; import type { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ServerForm } from './helpers/ServerForm'; @@ -44,12 +44,12 @@ const CreateServer: FCWithDeps = ({ servers const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME); const [isConfirmModalOpen, toggleConfirmModal] = useToggle(); const [serverData, setServerData] = useState(); - const saveNewServer = useCallback((theServerData: ServerData) => { - const id = randomUUID(); + const saveNewServer = useCallback((newServerData: ServerData) => { + const [newServerWithUniqueId] = ensureUniqueIds(servers, [newServerData]); - createServers([{ ...theServerData, id }]); - navigate(`/server/${id}`); - }, [createServers, navigate]); + createServers([newServerWithUniqueId]); + navigate(`/server/${newServerWithUniqueId.id}`); + }, [createServers, navigate, servers]); const onSubmit = useCallback((newServerData: ServerData) => { setServerData(newServerData); diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 161e49cdf..d4e8bee35 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -6,9 +6,10 @@ import { useCallback, useRef, useState } from 'react'; import { Button, UncontrolledTooltip } from 'reactstrap'; import type { FCWithDeps } from '../../container/utils'; import { componentFactory, useDependencies } from '../../container/utils'; -import type { ServerData, ServersMap } from '../data'; +import type { ServerData, ServersMap, ServerWithId } from '../data'; import type { ServersImporter } from '../services/ServersImporter'; import { DuplicatedServersModal } from './DuplicatedServersModal'; +import { dedupServers, ensureUniqueIds } from './index'; export type ImportServersBtnProps = PropsWithChildren<{ onImport?: () => void; @@ -18,7 +19,7 @@ export type ImportServersBtnProps = PropsWithChildren<{ }>; type ImportServersBtnConnectProps = ImportServersBtnProps & { - createServers: (servers: ServerData[]) => void; + createServers: (servers: ServerWithId[]) => void; servers: ServersMap; }; @@ -26,9 +27,6 @@ type ImportServersBtnDeps = { ServersImporter: ServersImporter }; -const serversInclude = (servers: ServerData[], { url, apiKey }: ServerData) => - servers.some((server) => server.url === url && server.apiKey === apiKey); - const ImportServersBtn: FCWithDeps = ({ createServers, servers, @@ -43,30 +41,31 @@ const ImportServersBtn: FCWithDeps([]); const [isModalOpen,, showModal, hideModal] = useToggle(); - const serversToCreate = useRef([]); - const create = useCallback((serversData: ServerData[]) => { + const importedServersRef = useRef([]); + const newServersRef = useRef([]); + + const create = useCallback((serversData: ServerWithId[]) => { createServers(serversData); onImport(); }, [createServers, onImport]); const onFile = useCallback( async ({ target }: ChangeEvent) => serversImporter.importServersFromFile(target.files?.[0]) - .then((newServers) => { - serversToCreate.current = newServers; + .then((importedServers) => { + const { duplicatedServers, newServers } = dedupServers(servers, importedServers); - const existingServers = Object.values(servers); - const dupServers = newServers.filter((server) => serversInclude(existingServers, server)); - const hasDuplicatedServers = !!dupServers.length; + importedServersRef.current = ensureUniqueIds(servers, importedServers); + newServersRef.current = ensureUniqueIds(servers, newServers); - if (!hasDuplicatedServers) { - create(newServers); + if (duplicatedServers.length === 0) { + create(importedServersRef.current); } else { - setDuplicatedServers(dupServers); + setDuplicatedServers(duplicatedServers); showModal(); } }) .then(() => { - // Reset input after processing file + // Reset file input after processing file (target as { value: string | null }).value = null; }) .catch(onImportError), @@ -74,13 +73,13 @@ const ImportServersBtn: FCWithDeps { - create(serversToCreate.current); + create(importedServersRef.current); hideModal(); - }, [create, hideModal, serversToCreate]); + }, [create, hideModal]); const createNonDuplicatedServers = useCallback(() => { - create(serversToCreate.current.filter((server) => !serversInclude(duplicatedServers, server))); + create(newServersRef.current); hideModal(); - }, [create, duplicatedServers, hideModal]); + }, [create, hideModal]); return ( <> @@ -91,7 +90,15 @@ const ImportServersBtn: FCWithDepsname, apiKey and url columns. - + 0) { + urlSegment = `${urlSegment} ${normalizedPathname}`; + } + } catch { + // If the server URL is not valid, use the value as is + } + + return `${server.name} ${urlSegment}`.toLowerCase().replace(/[^a-zA-Z0-9-_.~]/g, '-'); +} + +export function serversListToMap(servers: ServerWithId[]): ServersMap { + const serversMap: ServersMap = {}; + servers.forEach((server) => { + serversMap[server.id] = server; + }); + + return serversMap; +} + +const serversInclude = (serversList: ServerData[], { url, apiKey }: ServerData) => + serversList.some((server) => server.url === url && server.apiKey === apiKey); + +export type DedupServersResult = { + /** Servers which already exist in the reference list */ + duplicatedServers: ServerData[]; + /** Servers which are new based on a reference list */ + newServers: ServerData[]; +}; + +/** + * Given a list of new servers, checks which of them already exist in a servers map, and which don't + */ +export function dedupServers(servers: ServersMap, serversToAdd: ServerData[]): DedupServersResult { + const serversList = Object.values(servers); + const { duplicatedServers = [], newServers = [] } = groupBy( + serversToAdd, + (server) => serversInclude(serversList, server) ? 'duplicatedServers' : 'newServers', + ); + + return { duplicatedServers, newServers }; +} + +/** + * Given a servers map and a list of servers, return the same list of servers but all with an ID, ensuring the ID is + * unique both among all those servers and existing ones + */ +export function ensureUniqueIds(existingServers: ServersMap, serversList: ServerData[]): ServerWithId[] { + const existingIds = new Set(Object.keys(existingServers)); + const serversWithId: ServerWithId[] = []; + + serversList.forEach((server) => { + const baseId = idForServer(server); + + let id = baseId; + let iterations = 1; + while (existingIds.has(id)) { + id = `${baseId}-${iterations}`; + iterations++; + } + + serversWithId.push({ ...server, id }); + + // Add this server's ID to the list, so that it is taken into consideration for the next ones + existingIds.add(id); + }); + + return serversWithId; +} diff --git a/src/servers/reducers/remoteServers.ts b/src/servers/reducers/remoteServers.ts index 1ae5496cb..37c437d0c 100644 --- a/src/servers/reducers/remoteServers.ts +++ b/src/servers/reducers/remoteServers.ts @@ -1,11 +1,14 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import pack from '../../../package.json'; import { createAsyncThunk } from '../../utils/helpers/redux'; -import type { ServerData } from '../data'; import { hasServerData } from '../data'; +import { ensureUniqueIds } from '../helpers'; import { createServers } from './servers'; -const responseToServersList = (data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []); +const responseToServersList = (data: any) => ensureUniqueIds( + {}, + (Array.isArray(data) ? data.filter(hasServerData) : []), +); export const fetchServers = (httpClient: HttpClient) => createAsyncThunk( 'shlink/remoteServers/fetchServers', diff --git a/src/servers/reducers/servers.ts b/src/servers/reducers/servers.ts index fe8220e7b..ed02fe681 100644 --- a/src/servers/reducers/servers.ts +++ b/src/servers/reducers/servers.ts @@ -1,7 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { randomUUID } from '../../utils/utils'; import type { ServerData, ServersMap, ServerWithId } from '../data'; +import { serversListToMap } from '../helpers'; interface EditServer { serverId: string; @@ -15,19 +15,6 @@ interface SetAutoConnect { const initialState: ServersMap = {}; -const serverWithId = (server: ServerWithId | ServerData): ServerWithId => { - if ('id' in server) { - return server; - } - - return { ...server, id: randomUUID() }; -}; - -const serversListToMap = (servers: ServerWithId[]): ServersMap => servers.reduce( - (acc, server) => ({ ...acc, [server.id]: server }), - {}, -); - export const { actions, reducer } = createSlice({ name: 'shlink/servers', initialState, @@ -70,10 +57,7 @@ export const { actions, reducer } = createSlice({ }, }, createServers: { - prepare: (servers: ServerData[]) => { - const payload = serversListToMap(servers.map(serverWithId)); - return { payload }; - }, + prepare: (servers: ServerWithId[]) => ({ payload: serversListToMap(servers) }), reducer: (state, { payload: newServers }: PayloadAction) => ({ ...state, ...newServers }), }, }, diff --git a/src/utils/forms/FormText.tsx b/src/utils/forms/FormText.tsx deleted file mode 100644 index f80b5094a..000000000 --- a/src/utils/forms/FormText.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { FC, PropsWithChildren } from 'react'; - -export const FormText: FC> = ({ children }) => ( - {children} -); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d16080cc1..b1b11ca0c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,9 +1,6 @@ import type { SyntheticEvent } from 'react'; -import { v4 } from 'uuid'; export const handleEventPreventingDefault = (handler: () => T) => (e: SyntheticEvent) => { e.preventDefault(); handler(); }; - -export const randomUUID = () => v4(); diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index b3ba60c58..62a1888c5 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -1,6 +1,6 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import type { ServersMap, ServerWithId } from '../../../src/servers/data'; +import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data'; import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; import { ImportServersBtnFactory } from '../../../src/servers/helpers/ImportServersBtn'; @@ -9,6 +9,7 @@ import { checkAccessibility } from '../../__helpers__/accessibility'; import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { + const csvFile = new File([''], 'servers.csv', { type: 'text/csv' }); const onImportMock = vi.fn(); const createServersMock = vi.fn(); const importServersFromFile = vi.fn().mockResolvedValue([]); @@ -54,34 +55,43 @@ describe('', () => { }); it('imports servers when file input changes', async () => { - const { container } = setUp(); - const input = container.querySelector('[type=file]'); + const { user } = setUp(); + + const input = screen.getByTestId('csv-file-input'); + await user.upload(input, csvFile); - if (input) { - fireEvent.change(input, { target: { files: [''] } }); - } expect(importServersFromFile).toHaveBeenCalledTimes(1); - await waitFor(() => expect(createServersMock).toHaveBeenCalledTimes(1)); + expect(createServersMock).toHaveBeenCalledTimes(1); }); it.each([ - ['Save anyway', true], - ['Discard', false], - ])('creates expected servers depending on selected option in modal', async (btnName, savesDuplicatedServers) => { - const existingServer = fromPartial({ id: 'abc', url: 'existingUrl', apiKey: 'existingApiKey' }); - const newServer = fromPartial({ url: 'newUrl', apiKey: 'newApiKey' }); - const { container, user } = setUp({}, { abc: existingServer }); - const input = container.querySelector('[type=file]'); + { btnName: 'Save anyway',savesDuplicatedServers: true }, + { btnName: 'Discard', savesDuplicatedServers: false }, + ])('creates expected servers depending on selected option in modal', async ({ btnName, savesDuplicatedServers }) => { + const existingServerData: ServerData = { + name: 'existingServer', + url: 'http://s.test/existingUrl', + apiKey: 'existingApiKey', + }; + const existingServer: ServerWithId = { + ...existingServerData, + id: 'existingserver-s.test', + }; + const newServer: ServerData = { name: 'newServer', url: 'http://s.test/newUrl', apiKey: 'newApiKey' }; + const { user } = setUp({}, { [existingServer.id]: existingServer }); + importServersFromFile.mockResolvedValue([existingServer, newServer]); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - if (input) { - fireEvent.change(input, { target: { files: [''] } }); - } - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + await user.upload(screen.getByTestId('csv-file-input'), csvFile); + expect(screen.getByRole('dialog')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: btnName })); - expect(createServersMock).toHaveBeenCalledWith(savesDuplicatedServers ? [existingServer, newServer] : [newServer]); + expect(createServersMock).toHaveBeenCalledWith( + savesDuplicatedServers + ? [expect.objectContaining(existingServerData), expect.objectContaining(newServer)] + : [expect.objectContaining(newServer)], + ); expect(onImportMock).toHaveBeenCalledTimes(1); }); }); diff --git a/test/servers/helpers/index.test.ts b/test/servers/helpers/index.test.ts new file mode 100644 index 000000000..370f6c6c3 --- /dev/null +++ b/test/servers/helpers/index.test.ts @@ -0,0 +1,69 @@ +import { fromPartial } from '@total-typescript/shoehorn'; +import type { ServersMap } from '../../../src/servers/data'; +import { ensureUniqueIds } from '../../../src/servers/helpers'; + +describe('index', () => { + describe('ensureUniqueIds', () => { + const servers: ServersMap = { + 'the-name-example.com': fromPartial({}), + 'another-name-example.com': fromPartial({}), + 'short-domain-s.test': fromPartial({}), + }; + + it('returns expected list of servers when existing IDs conflict', () => { + const result = ensureUniqueIds(servers, [ + fromPartial({ name: 'The name', url: 'https://example.com' }), + fromPartial({ name: 'Short domain', url: 'https://s.test' }), + fromPartial({ name: 'The name', url: 'https://example.com' }), + ]); + + expect(result).toEqual([ + expect.objectContaining({ id: 'the-name-example.com-1' }), + expect.objectContaining({ id: 'short-domain-s.test-1' }), + expect.objectContaining({ id: 'the-name-example.com-2' }), + ]); + }); + + it('returns expected list of servers when IDs conflict in provided list of servers', () => { + const result = ensureUniqueIds(servers, [ + fromPartial({ name: 'Foo', url: 'https://example.com' }), + fromPartial({ name: 'Bar', url: 'https://s.test' }), + fromPartial({ name: 'Foo', url: 'https://example.com' }), + fromPartial({ name: 'Baz', url: 'https://s.test' }), + ]); + + expect(result).toEqual([ + expect.objectContaining({ id: 'foo-example.com' }), + expect.objectContaining({ id: 'bar-s.test' }), + expect.objectContaining({ id: 'foo-example.com-1' }), + expect.objectContaining({ id: 'baz-s.test' }), + ]); + }); + + it('includes server paths when not empty', () => { + const result = ensureUniqueIds({}, [ + fromPartial({ name: 'Foo', url: 'https://example.com' }), + fromPartial({ name: 'Bar', url: 'https://s.test/some/path' }), + fromPartial({ name: 'Baz', url: 'https://s.test/some/other-path-here/123' }), + ]); + + expect(result).toEqual([ + expect.objectContaining({ id: 'foo-example.com' }), + expect.objectContaining({ id: 'bar-s.test-some-path' }), + expect.objectContaining({ id: 'baz-s.test-some-other-path-here-123' }), + ]); + }); + + it('uses server URL verbatim when it is not a valid URL', () => { + const result = ensureUniqueIds({}, [ + fromPartial({ name: 'Foo', url: 'invalid' }), + fromPartial({ name: 'Bar', url: 'this is not a URL' }), + ]); + + expect(result).toEqual([ + expect.objectContaining({ id: 'foo-invalid' }), + expect.objectContaining({ id: 'bar-this-is-not-a-url' }), + ]); + }); + }); +}); diff --git a/test/servers/reducers/remoteServers.test.ts b/test/servers/reducers/remoteServers.test.ts index 1026cbcd4..3c0cf580f 100644 --- a/test/servers/reducers/remoteServers.test.ts +++ b/test/servers/reducers/remoteServers.test.ts @@ -9,74 +9,76 @@ describe('remoteServersReducer', () => { const httpClient = fromPartial({ jsonRequest }); it.each([ - [ - [ + { + serversArray: [ { - id: '111', name: 'acel.me from servers.json', url: 'https://acel.me', apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0', }, { - id: '222', name: 'Local from servers.json', url: 'http://localhost:8000', apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a', }, ], - { - 111: { - id: '111', + expectedNewServers: { + 'acel.me-from-servers.json-acel.me': { + id: 'acel.me-from-servers.json-acel.me', name: 'acel.me from servers.json', url: 'https://acel.me', apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0', }, - 222: { - id: '222', + 'local-from-servers.json-localhost-8000': { + id: 'local-from-servers.json-localhost-8000', name: 'Local from servers.json', url: 'http://localhost:8000', apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a', }, }, - ], - [ - [ + }, + { + serversArray: [ { - id: '111', name: 'acel.me from servers.json', url: 'https://acel.me', apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0', }, { - id: '222', name: 'Invalid', }, { - id: '333', name: 'Local from servers.json', url: 'http://localhost:8000', apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a', }, ], - { - 111: { - id: '111', + expectedNewServers: { + 'acel.me-from-servers.json-acel.me': { + id: 'acel.me-from-servers.json-acel.me', name: 'acel.me from servers.json', url: 'https://acel.me', apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0', }, - 333: { - id: '333', + 'local-from-servers.json-localhost-8000': { + id: 'local-from-servers.json-localhost-8000', name: 'Local from servers.json', url: 'http://localhost:8000', apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a', }, + }, - ], - ['', {}], - [{}, {}], - ])('tries to fetch servers from remote', async (mockedValue, expectedNewServers) => { - jsonRequest.mockResolvedValue(mockedValue); + }, + { + serversArray: '', + expectedNewServers: {}, + }, + { + serversArray: {}, + expectedNewServers: {}, + }, + ])('tries to fetch servers from remote', async ({ serversArray, expectedNewServers }) => { + jsonRequest.mockResolvedValue(serversArray); const doFetchServers = fetchServers(httpClient); await doFetchServers()(dispatch, vi.fn(), {}); diff --git a/test/servers/reducers/selectedServer.test.ts b/test/servers/reducers/selectedServer.test.ts index fac4b7083..8c6020526 100644 --- a/test/servers/reducers/selectedServer.test.ts +++ b/test/servers/reducers/selectedServer.test.ts @@ -9,7 +9,6 @@ import { selectedServerReducerCreator, selectServer as selectServerCreator, } from '../../../src/servers/reducers/selectedServer'; -import { randomUUID } from '../../../src/utils/utils'; describe('selectedServerReducer', () => { const dispatch = vi.fn(); @@ -41,7 +40,7 @@ describe('selectedServerReducer', () => { ['latest', MAX_FALLBACK_VERSION, 'latest'], ['%invalid_semver%', MIN_FALLBACK_VERSION, '%invalid_semver%'], ])('dispatches proper actions', async (serverVersion, expectedVersion, expectedPrintableVersion) => { - const id = randomUUID(); + const id = crypto.randomUUID(); const getState = createGetStateMock(id); const expectedSelectedServer = { id, @@ -60,7 +59,7 @@ describe('selectedServerReducer', () => { }); it('dispatches error when health endpoint fails', async () => { - const id = randomUUID(); + const id = crypto.randomUUID(); const getState = createGetStateMock(id); const expectedSelectedServer = fromPartial({ id, serverNotReachable: true }); @@ -73,7 +72,7 @@ describe('selectedServerReducer', () => { }); it('dispatches error when server is not found', async () => { - const id = randomUUID(); + const id = crypto.randomUUID(); const getState = vi.fn(() => fromPartial({ servers: {} })); const expectedSelectedServer: NotFoundServer = { serverNotFound: true }; diff --git a/test/servers/reducers/servers.test.ts b/test/servers/reducers/servers.test.ts index 025a44a17..fcb4642a9 100644 --- a/test/servers/reducers/servers.test.ts +++ b/test/servers/reducers/servers.test.ts @@ -105,15 +105,6 @@ describe('serversReducer', () => { expect(payload).toEqual(list); }); - - it('generates an id for every provided server if they do not have it', () => { - const servers = Object.values(list).map(({ name, autoConnect, url, apiKey }) => ( - { name, autoConnect, url, apiKey } - )); - const { payload } = createServers(servers); - - expect(Object.values(payload).every(({ id }) => !!id)).toEqual(true); - }); }); describe('setAutoConnect', () => {