mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-04-11 17:16:20 +00:00
Merge pull request #550 from acelaya-forks/feature/domain-health-checks
Feature/domain health checks
This commit is contained in:
@@ -12,6 +12,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
If the resulting list for that interval is empty, it will try to infer the closest interval with visits, based on the latest visit's date, and reload visits for that interval.
|
If the resulting list for that interval is empty, it will try to infer the closest interval with visits, based on the latest visit's date, and reload visits for that interval.
|
||||||
|
|
||||||
|
* [#547](https://github.com/shlinkio/shlink-web-client/pull/547) Improved domains page, to tell which of the domains are not properly configured.
|
||||||
|
|
||||||
|
Now, when this section is loaded, it tries to call the `GET /rest/health` endpoint for each one of the domains, and displays a warning icon on each one that failed.
|
||||||
|
|
||||||
|
The warning includes a link to the documentation, explaining what are the steps to get it fixed.
|
||||||
|
|
||||||
* [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer.
|
* [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer.
|
||||||
* [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page.
|
* [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page.
|
||||||
* [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs.
|
* [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs.
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import { FC } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import {
|
import {
|
||||||
faBan as forbiddenIcon,
|
faBan as forbiddenIcon,
|
||||||
faCheck as defaultDomainIcon,
|
faDotCircle as defaultDomainIcon,
|
||||||
faEdit as editIcon,
|
faEdit as editIcon,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../api/types';
|
import { ShlinkDomainRedirects } from '../api/types';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import { OptionalString } from '../utils/utils';
|
import { OptionalString } from '../utils/utils';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { supportsDefaultDomainRedirectsEdition } from '../utils/helpers/features';
|
import { supportsDefaultDomainRedirectsEdition } from '../utils/helpers/features';
|
||||||
import { EditDomainRedirectsModal } from './helpers/EditDomainRedirectsModal';
|
import { EditDomainRedirectsModal } from './helpers/EditDomainRedirectsModal';
|
||||||
|
import { Domain } from './data';
|
||||||
|
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
||||||
|
|
||||||
interface DomainRowProps {
|
interface DomainRowProps {
|
||||||
domain: ShlinkDomain;
|
domain: Domain;
|
||||||
defaultRedirects?: ShlinkDomainRedirects;
|
defaultRedirects?: ShlinkDomainRedirects;
|
||||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||||
|
checkDomainHealth: (domain: string) => void;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,14 +36,20 @@ const DefaultDomain: FC = () => (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DomainRow: FC<DomainRowProps> = ({ domain, editDomainRedirects, defaultRedirects, selectedServer }) => {
|
export const DomainRow: FC<DomainRowProps> = (
|
||||||
|
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects, selectedServer },
|
||||||
|
) => {
|
||||||
const [ isOpen, toggle ] = useToggle();
|
const [ isOpen, toggle ] = useToggle();
|
||||||
const { domain: authority, isDefault, redirects } = domain;
|
const { domain: authority, isDefault, redirects, status } = domain;
|
||||||
const canEditDomain = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
|
const canEditDomain = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkDomainHealth(domain.domain);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="responsive-table__row">
|
<tr className="responsive-table__row">
|
||||||
<td className="responsive-table__cell" data-th="Is default domain">{isDefault ? <DefaultDomain /> : ''}</td>
|
<td className="responsive-table__cell" data-th="Is default domain">{isDefault && <DefaultDomain />}</td>
|
||||||
<th className="responsive-table__cell" data-th="Domain">{authority}</th>
|
<th className="responsive-table__cell" data-th="Domain">{authority}</th>
|
||||||
<td className="responsive-table__cell" data-th="Base path redirect">
|
<td className="responsive-table__cell" data-th="Base path redirect">
|
||||||
{redirects?.baseUrlRedirect ?? <Nr fallback={defaultRedirects?.baseUrlRedirect} />}
|
{redirects?.baseUrlRedirect ?? <Nr fallback={defaultRedirects?.baseUrlRedirect} />}
|
||||||
@@ -51,6 +60,9 @@ export const DomainRow: FC<DomainRowProps> = ({ domain, editDomainRedirects, def
|
|||||||
<td className="responsive-table__cell" data-th="Invalid short URL redirect">
|
<td className="responsive-table__cell" data-th="Invalid short URL redirect">
|
||||||
{redirects?.invalidShortUrlRedirect ?? <Nr fallback={defaultRedirects?.invalidShortUrlRedirect} />}
|
{redirects?.invalidShortUrlRedirect ?? <Nr fallback={defaultRedirects?.invalidShortUrlRedirect} />}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="responsive-table__cell text-lg-center" data-th="Status">
|
||||||
|
<DomainStatusIcon status={status} />
|
||||||
|
</td>
|
||||||
<td className="responsive-table__cell text-right">
|
<td className="responsive-table__cell text-right">
|
||||||
<span id={!canEditDomain ? 'defaultDomainBtn' : undefined}>
|
<span id={!canEditDomain ? 'defaultDomainBtn' : undefined}>
|
||||||
<Button outline size="sm" disabled={!canEditDomain} onClick={!canEditDomain ? undefined : toggle}>
|
<Button outline size="sm" disabled={!canEditDomain} onClick={!canEditDomain ? undefined : toggle}>
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ interface ManageDomainsProps {
|
|||||||
listDomains: Function;
|
listDomains: Function;
|
||||||
filterDomains: (searchTerm: string) => void;
|
filterDomains: (searchTerm: string) => void;
|
||||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||||
|
checkDomainHealth: (domain: string) => void;
|
||||||
domainsList: DomainsList;
|
domainsList: DomainsList;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = [ '', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '' ];
|
const headers = [ '', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', '' ];
|
||||||
|
|
||||||
export const ManageDomains: FC<ManageDomainsProps> = (
|
export const ManageDomains: FC<ManageDomainsProps> = (
|
||||||
{ listDomains, domainsList, filterDomains, editDomainRedirects, selectedServer },
|
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth, selectedServer },
|
||||||
) => {
|
) => {
|
||||||
const { filteredDomains: domains, defaultRedirects, loading, error, errorData } = domainsList;
|
const { filteredDomains: domains, defaultRedirects, loading, error, errorData } = domainsList;
|
||||||
const resolvedDefaultRedirects = defaultRedirects ?? domains.find(({ isDefault }) => isDefault)?.redirects;
|
const resolvedDefaultRedirects = defaultRedirects ?? domains.find(({ isDefault }) => isDefault)?.redirects;
|
||||||
@@ -55,6 +56,7 @@ export const ManageDomains: FC<ManageDomainsProps> = (
|
|||||||
key={domain.domain}
|
key={domain.domain}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
editDomainRedirects={editDomainRedirects}
|
editDomainRedirects={editDomainRedirects}
|
||||||
|
checkDomainHealth={checkDomainHealth}
|
||||||
defaultRedirects={resolvedDefaultRedirects}
|
defaultRedirects={resolvedDefaultRedirects}
|
||||||
selectedServer={selectedServer}
|
selectedServer={selectedServer}
|
||||||
/>
|
/>
|
||||||
|
|||||||
7
src/domains/data/index.ts
Normal file
7
src/domains/data/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { ShlinkDomain } from '../../api/types';
|
||||||
|
|
||||||
|
export type DomainStatus = 'validating' | 'valid' | 'invalid';
|
||||||
|
|
||||||
|
export interface Domain extends ShlinkDomain {
|
||||||
|
status: DomainStatus;
|
||||||
|
}
|
||||||
62
src/domains/helpers/DomainStatusIcon.tsx
Normal file
62
src/domains/helpers/DomainStatusIcon.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { FC, useEffect, useRef, useState } from 'react';
|
||||||
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import {
|
||||||
|
faTimes as invalidIcon,
|
||||||
|
faCheck as checkIcon,
|
||||||
|
faCircleNotch as loadingStatusIcon,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { MediaMatcher } from '../../utils/types';
|
||||||
|
import { DomainStatus } from '../data';
|
||||||
|
|
||||||
|
interface DomainStatusIconProps {
|
||||||
|
status: DomainStatus;
|
||||||
|
matchMedia?: MediaMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia = window.matchMedia }) => {
|
||||||
|
const ref = useRef<HTMLSpanElement>();
|
||||||
|
const matchesMobile = () => matchMedia('(max-width: 991px)').matches;
|
||||||
|
const [ isMobile, setIsMobile ] = useState<boolean>(matchesMobile());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = () => setIsMobile(matchesMobile());
|
||||||
|
|
||||||
|
window.addEventListener('resize', listener);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', listener);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (status === 'validating') {
|
||||||
|
return <FontAwesomeIcon fixedWidth icon={loadingStatusIcon} spin />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
ref={(el: HTMLSpanElement) => {
|
||||||
|
ref.current = el;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status === 'valid'
|
||||||
|
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
|
||||||
|
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}
|
||||||
|
</span>
|
||||||
|
<UncontrolledTooltip
|
||||||
|
target={(() => ref.current) as any}
|
||||||
|
placement={isMobile ? 'top-start' : 'left'}
|
||||||
|
autohide={status === 'valid'}
|
||||||
|
>
|
||||||
|
{status === 'valid' ? 'Congratulations! This domain is properly configured.' : (
|
||||||
|
<span>
|
||||||
|
Oops! There is some missing configuration, and short URLs shared with this domain will not work.
|
||||||
|
<br />
|
||||||
|
Check the <ExternalLink href="https://slnk.to/multi-domain-docs">documentation</ExternalLink> in order to
|
||||||
|
find out what is missing.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</UncontrolledTooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { ProblemDetailsError, ShlinkDomain, ShlinkDomainRedirects } from '../../api/types';
|
import { ProblemDetailsError, ShlinkDomainRedirects } from '../../api/types';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { ApiErrorAction } from '../../api/types/actions';
|
||||||
|
import { Domain, DomainStatus } from '../data';
|
||||||
|
import { hasServerData } from '../../servers/data';
|
||||||
|
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
||||||
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
|
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
@@ -12,11 +15,12 @@ export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
|
|||||||
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
|
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
|
||||||
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
|
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
|
||||||
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
|
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
|
||||||
|
export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
|
||||||
/* eslint-enable padding-line-between-statements */
|
/* eslint-enable padding-line-between-statements */
|
||||||
|
|
||||||
export interface DomainsList {
|
export interface DomainsList {
|
||||||
domains: ShlinkDomain[];
|
domains: Domain[];
|
||||||
filteredDomains: ShlinkDomain[];
|
filteredDomains: Domain[];
|
||||||
defaultRedirects?: ShlinkDomainRedirects;
|
defaultRedirects?: ShlinkDomainRedirects;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
@@ -24,7 +28,7 @@ export interface DomainsList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ListDomainsAction extends Action<string> {
|
export interface ListDomainsAction extends Action<string> {
|
||||||
domains: ShlinkDomain[];
|
domains: Domain[];
|
||||||
defaultRedirects?: ShlinkDomainRedirects;
|
defaultRedirects?: ShlinkDomainRedirects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +36,11 @@ interface FilterDomainsAction extends Action<string> {
|
|||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ValidateDomain extends Action<string> {
|
||||||
|
domain: string;
|
||||||
|
status: DomainStatus;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: DomainsList = {
|
const initialState: DomainsList = {
|
||||||
domains: [],
|
domains: [],
|
||||||
filteredDomains: [],
|
filteredDomains: [],
|
||||||
@@ -42,10 +51,14 @@ const initialState: DomainsList = {
|
|||||||
export type DomainsCombinedAction = ListDomainsAction
|
export type DomainsCombinedAction = ListDomainsAction
|
||||||
& ApiErrorAction
|
& ApiErrorAction
|
||||||
& FilterDomainsAction
|
& FilterDomainsAction
|
||||||
& EditDomainRedirectsAction;
|
& EditDomainRedirectsAction
|
||||||
|
& ValidateDomain;
|
||||||
|
|
||||||
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
|
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
|
||||||
(d: ShlinkDomain): ShlinkDomain => d.domain !== domain ? d : { ...d, redirects };
|
(d: Domain): Domain => d.domain !== domain ? d : { ...d, redirects };
|
||||||
|
|
||||||
|
export const replaceStatusOnDomain = (domain: string, status: DomainStatus) =>
|
||||||
|
(d: Domain): Domain => d.domain !== domain ? d : { ...d, status };
|
||||||
|
|
||||||
export default buildReducer<DomainsList, DomainsCombinedAction>({
|
export default buildReducer<DomainsList, DomainsCombinedAction>({
|
||||||
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
|
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
|
||||||
@@ -61,6 +74,11 @@ export default buildReducer<DomainsList, DomainsCombinedAction>({
|
|||||||
domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)),
|
domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||||
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
|
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||||
}),
|
}),
|
||||||
|
[VALIDATE_DOMAIN]: (state, { domain, status }) => ({
|
||||||
|
...state,
|
||||||
|
domains: state.domains.map(replaceStatusOnDomain(domain, status)),
|
||||||
|
filteredDomains: state.filteredDomains.map(replaceStatusOnDomain(domain, status)),
|
||||||
|
}),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
|
export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
|
||||||
@@ -71,7 +89,10 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => ()
|
|||||||
const { listDomains } = buildShlinkApiClient(getState);
|
const { listDomains } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: domains, defaultRedirects } = await listDomains();
|
const { domains, defaultRedirects } = await listDomains().then(({ data, defaultRedirects }) => ({
|
||||||
|
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
|
||||||
|
defaultRedirects,
|
||||||
|
}));
|
||||||
|
|
||||||
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains, defaultRedirects });
|
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains, defaultRedirects });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -80,3 +101,30 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => ()
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const filterDomains = (searchTerm: string): FilterDomainsAction => ({ type: FILTER_DOMAINS, searchTerm });
|
export const filterDomains = (searchTerm: string): FilterDomainsAction => ({ type: FILTER_DOMAINS, searchTerm });
|
||||||
|
|
||||||
|
export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) => (domain: string) => async (
|
||||||
|
dispatch: Dispatch,
|
||||||
|
getState: GetState,
|
||||||
|
) => {
|
||||||
|
const { selectedServer } = getState();
|
||||||
|
|
||||||
|
if (!hasServerData(selectedServer)) {
|
||||||
|
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { url, ...rest } = selectedServer;
|
||||||
|
const { health } = buildShlinkApiClient({
|
||||||
|
...rest,
|
||||||
|
url: replaceAuthorityFromUri(url, domain),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status } = await health();
|
||||||
|
|
||||||
|
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: status === 'pass' ? 'valid' : 'invalid' });
|
||||||
|
} catch (e) {
|
||||||
|
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { filterDomains, listDomains } from '../reducers/domainsList';
|
import { checkDomainHealth, filterDomains, listDomains } from '../reducers/domainsList';
|
||||||
import { DomainSelector } from '../DomainSelector';
|
import { DomainSelector } from '../DomainSelector';
|
||||||
import { ManageDomains } from '../ManageDomains';
|
import { ManageDomains } from '../ManageDomains';
|
||||||
import { editDomainRedirects } from '../reducers/domainRedirects';
|
import { editDomainRedirects } from '../reducers/domainRedirects';
|
||||||
@@ -13,13 +13,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
||||||
bottle.decorator('ManageDomains', connect(
|
bottle.decorator('ManageDomains', connect(
|
||||||
[ 'domainsList', 'selectedServer' ],
|
[ 'domainsList', 'selectedServer' ],
|
||||||
[ 'listDomains', 'filterDomains', 'editDomainRedirects' ],
|
[ 'listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
|
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('filterDomains', () => filterDomains);
|
bottle.serviceFactory('filterDomains', () => filterDomains);
|
||||||
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
|
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
|
||||||
|
bottle.serviceFactory('checkDomainHealth', checkDomainHealth, 'buildShlinkApiClient');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|||||||
7
src/utils/helpers/uri.ts
Normal file
7
src/utils/helpers/uri.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const replaceAuthorityFromUri = (uri: string, newAuthority: string): string => {
|
||||||
|
const [ schema, rest ] = uri.split('://');
|
||||||
|
const [ , ...pathParts ] = rest.split('/');
|
||||||
|
const normalizedPath = pathParts.length ? `/${pathParts.join('/')}` : '';
|
||||||
|
|
||||||
|
return `${schema}://${newAuthority}${normalizedPath}`;
|
||||||
|
};
|
||||||
1
src/utils/types.ts
Normal file
1
src/utils/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type MediaMatcher = (query: string) => MediaQueryList;
|
||||||
@@ -12,6 +12,7 @@ import { supportsBotVisits } from '../utils/helpers/features';
|
|||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { Time } from '../utils/Time';
|
import { Time } from '../utils/Time';
|
||||||
import { TableOrderIcon } from '../utils/table/TableOrderIcon';
|
import { TableOrderIcon } from '../utils/table/TableOrderIcon';
|
||||||
|
import { MediaMatcher } from '../utils/types';
|
||||||
import { NormalizedOrphanVisit, NormalizedVisit } from './types';
|
import { NormalizedOrphanVisit, NormalizedVisit } from './types';
|
||||||
import './VisitsTable.scss';
|
import './VisitsTable.scss';
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ export interface VisitsTableProps {
|
|||||||
visits: NormalizedVisit[];
|
visits: NormalizedVisit[];
|
||||||
selectedVisits?: NormalizedVisit[];
|
selectedVisits?: NormalizedVisit[];
|
||||||
setSelectedVisits: (visits: NormalizedVisit[]) => void;
|
setSelectedVisits: (visits: NormalizedVisit[]) => void;
|
||||||
matchMedia?: (query: string) => MediaQueryList;
|
matchMedia?: MediaMatcher;
|
||||||
isOrphanVisits?: boolean;
|
isOrphanVisits?: boolean;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,22 @@ import { Mock } from 'ts-mockery';
|
|||||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faBan as forbiddenIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faBan as forbiddenIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../../src/api/types';
|
import { ShlinkDomainRedirects } from '../../src/api/types';
|
||||||
import { DomainRow } from '../../src/domains/DomainRow';
|
import { DomainRow } from '../../src/domains/DomainRow';
|
||||||
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||||
|
import { Domain } from '../../src/domains/data';
|
||||||
|
|
||||||
describe('<DomainRow />', () => {
|
describe('<DomainRow />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const createWrapper = (domain: ShlinkDomain, selectedServer = Mock.all<SelectedServer>()) => {
|
const createWrapper = (domain: Domain, selectedServer = Mock.all<SelectedServer>()) => {
|
||||||
wrapper = shallow(<DomainRow domain={domain} editDomainRedirects={jest.fn()} selectedServer={selectedServer} />);
|
wrapper = shallow(
|
||||||
|
<DomainRow
|
||||||
|
domain={domain}
|
||||||
|
selectedServer={selectedServer}
|
||||||
|
editDomainRedirects={jest.fn()}
|
||||||
|
checkDomainHealth={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
@@ -18,34 +26,34 @@ describe('<DomainRow />', () => {
|
|||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ Mock.of<ShlinkDomain>({ domain: '', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ],
|
[ Mock.of<Domain>({ domain: '', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ],
|
||||||
[ Mock.of<ShlinkDomain>({ domain: '', isDefault: false }), undefined, 0, 0, undefined ],
|
[ Mock.of<Domain>({ domain: '', isDefault: false }), undefined, 0, 0, undefined ],
|
||||||
[ Mock.of<ShlinkDomain>({ domain: 'foo.com', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ],
|
[ Mock.of<Domain>({ domain: 'foo.com', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ],
|
||||||
[ Mock.of<ShlinkDomain>({ domain: 'foo.bar.com', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ],
|
[ Mock.of<Domain>({ domain: 'foo.bar.com', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ],
|
||||||
[ Mock.of<ShlinkDomain>({ domain: 'foo.baz', isDefault: false }), undefined, 0, 0, undefined ],
|
[ Mock.of<Domain>({ domain: 'foo.baz', isDefault: false }), undefined, 0, 0, undefined ],
|
||||||
[
|
[
|
||||||
Mock.of<ShlinkDomain>({ domain: 'foo.baz', isDefault: true }),
|
Mock.of<Domain>({ domain: 'foo.baz', isDefault: true }),
|
||||||
Mock.of<ReachableServer>({ version: '2.10.0' }),
|
Mock.of<ReachableServer>({ version: '2.10.0' }),
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Mock.of<ShlinkDomain>({ domain: 'foo.baz', isDefault: true }),
|
Mock.of<Domain>({ domain: 'foo.baz', isDefault: true }),
|
||||||
Mock.of<ReachableServer>({ version: '2.9.0' }),
|
Mock.of<ReachableServer>({ version: '2.9.0' }),
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
'defaultDomainBtn',
|
'defaultDomainBtn',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Mock.of<ShlinkDomain>({ domain: 'foo.baz', isDefault: false }),
|
Mock.of<Domain>({ domain: 'foo.baz', isDefault: false }),
|
||||||
Mock.of<ReachableServer>({ version: '2.9.0' }),
|
Mock.of<ReachableServer>({ version: '2.9.0' }),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Mock.of<ShlinkDomain>({ domain: 'foo.baz', isDefault: false }),
|
Mock.of<Domain>({ domain: 'foo.baz', isDefault: false }),
|
||||||
Mock.of<ReachableServer>({ version: '2.10.0' }),
|
Mock.of<ReachableServer>({ version: '2.10.0' }),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -89,7 +97,7 @@ describe('<DomainRow />', () => {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
])('shows expected redirects', (redirects, expectedNoRedirects) => {
|
])('shows expected redirects', (redirects, expectedNoRedirects) => {
|
||||||
const wrapper = createWrapper(Mock.of<ShlinkDomain>({ domain: '', isDefault: true, redirects }));
|
const wrapper = createWrapper(Mock.of<Domain>({ domain: '', isDefault: true, redirects }));
|
||||||
const noRedirects = wrapper.find('Nr');
|
const noRedirects = wrapper.find('Nr');
|
||||||
const cells = wrapper.find('td');
|
const cells = wrapper.find('td');
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import { SelectedServer } from '../../src/servers/data';
|
|||||||
describe('<ManageDomains />', () => {
|
describe('<ManageDomains />', () => {
|
||||||
const listDomains = jest.fn();
|
const listDomains = jest.fn();
|
||||||
const filterDomains = jest.fn();
|
const filterDomains = jest.fn();
|
||||||
const editDomainRedirects = jest.fn();
|
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const createWrapper = (domainsList: DomainsList) => {
|
const createWrapper = (domainsList: DomainsList) => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<ManageDomains
|
<ManageDomains
|
||||||
listDomains={listDomains}
|
listDomains={listDomains}
|
||||||
filterDomains={filterDomains}
|
filterDomains={filterDomains}
|
||||||
editDomainRedirects={editDomainRedirects}
|
editDomainRedirects={jest.fn()}
|
||||||
|
checkDomainHealth={jest.fn()}
|
||||||
domainsList={domainsList}
|
domainsList={domainsList}
|
||||||
selectedServer={Mock.all<SelectedServer>()}
|
selectedServer={Mock.all<SelectedServer>()}
|
||||||
/>,
|
/>,
|
||||||
@@ -77,7 +77,7 @@ describe('<ManageDomains />', () => {
|
|||||||
const wrapper = createWrapper(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] }));
|
const wrapper = createWrapper(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] }));
|
||||||
const headerCells = wrapper.find('th');
|
const headerCells = wrapper.find('th');
|
||||||
|
|
||||||
expect(headerCells).toHaveLength(6);
|
expect(headerCells).toHaveLength(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('one row when list of domains is empty', () => {
|
it('one row when list of domains is empty', () => {
|
||||||
|
|||||||
73
test/domains/helpers/DomainStatusIcon.test.tsx
Normal file
73
test/domains/helpers/DomainStatusIcon.test.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
|
import { faTimes, faCheck, faCircleNotch } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { DomainStatus } from '../../../src/domains/data';
|
||||||
|
import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon';
|
||||||
|
|
||||||
|
describe('<DomainStatusIcon />', () => {
|
||||||
|
const matchMedia = jest.fn().mockReturnValue(Mock.of<MediaQueryList>({ matches: false }));
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
const createWrapper = (status: DomainStatus) => {
|
||||||
|
wrapper = shallow(<DomainStatusIcon status={status} matchMedia={matchMedia} />);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(jest.clearAllMocks);
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
it('renders loading icon when status is "validating"', () => {
|
||||||
|
const wrapper = createWrapper('validating');
|
||||||
|
const tooltip = wrapper.find(UncontrolledTooltip);
|
||||||
|
const faIcon = wrapper.find(FontAwesomeIcon);
|
||||||
|
|
||||||
|
expect(tooltip).toHaveLength(0);
|
||||||
|
expect(faIcon).toHaveLength(1);
|
||||||
|
expect(faIcon.prop('icon')).toEqual(faCircleNotch);
|
||||||
|
expect(faIcon.prop('spin')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
'invalid' as DomainStatus,
|
||||||
|
faTimes,
|
||||||
|
'Oops! There is some missing configuration, and short URLs shared with this domain will not work.',
|
||||||
|
],
|
||||||
|
[ 'valid' as DomainStatus, faCheck, 'Congratulations! This domain is properly configured.' ],
|
||||||
|
])('renders expected icon and tooltip when status is not validating', (status, expectedIcon, expectedText) => {
|
||||||
|
const wrapper = createWrapper(status);
|
||||||
|
const tooltip = wrapper.find(UncontrolledTooltip);
|
||||||
|
const faIcon = wrapper.find(FontAwesomeIcon);
|
||||||
|
const getTooltipText = (): string => {
|
||||||
|
const children = tooltip.prop('children');
|
||||||
|
|
||||||
|
if (typeof children === 'string') {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltip.find('span').html();
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(tooltip).toHaveLength(1);
|
||||||
|
expect(tooltip.prop('autohide')).toEqual(status === 'valid');
|
||||||
|
expect(getTooltipText()).toContain(expectedText);
|
||||||
|
expect(faIcon).toHaveLength(1);
|
||||||
|
expect(faIcon.prop('icon')).toEqual(expectedIcon);
|
||||||
|
expect(faIcon.prop('spin')).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ true, 'top-start' ],
|
||||||
|
[ false, 'left' ],
|
||||||
|
])('places the tooltip properly based on query match', (isMobile, expectedPlacement) => {
|
||||||
|
matchMedia.mockReturnValue(Mock.of<MediaQueryList>({ matches: isMobile }));
|
||||||
|
|
||||||
|
const wrapper = createWrapper('valid');
|
||||||
|
const tooltip = wrapper.find(UncontrolledTooltip);
|
||||||
|
|
||||||
|
expect(tooltip).toHaveLength(1);
|
||||||
|
expect(tooltip.prop('placement')).toEqual(expectedPlacement);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,19 +4,35 @@ import reducer, {
|
|||||||
LIST_DOMAINS_ERROR,
|
LIST_DOMAINS_ERROR,
|
||||||
LIST_DOMAINS_START,
|
LIST_DOMAINS_START,
|
||||||
FILTER_DOMAINS,
|
FILTER_DOMAINS,
|
||||||
|
VALIDATE_DOMAIN,
|
||||||
DomainsCombinedAction,
|
DomainsCombinedAction,
|
||||||
DomainsList,
|
DomainsList,
|
||||||
listDomains as listDomainsAction,
|
listDomains as listDomainsAction,
|
||||||
filterDomains as filterDomainsAction,
|
filterDomains as filterDomainsAction,
|
||||||
replaceRedirectsOnDomain,
|
replaceRedirectsOnDomain,
|
||||||
|
checkDomainHealth,
|
||||||
|
replaceStatusOnDomain,
|
||||||
} from '../../../src/domains/reducers/domainsList';
|
} from '../../../src/domains/reducers/domainsList';
|
||||||
import { EDIT_DOMAIN_REDIRECTS } from '../../../src/domains/reducers/domainRedirects';
|
import { EDIT_DOMAIN_REDIRECTS } from '../../../src/domains/reducers/domainRedirects';
|
||||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../../../src/api/types';
|
import { ShlinkDomainRedirects } from '../../../src/api/types';
|
||||||
import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
|
import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
|
||||||
|
import { Domain } from '../../../src/domains/data';
|
||||||
|
import { ShlinkState } from '../../../src/container/types';
|
||||||
|
import { SelectedServer, ServerData } from '../../../src/servers/data';
|
||||||
|
|
||||||
describe('domainsListReducer', () => {
|
describe('domainsListReducer', () => {
|
||||||
const filteredDomains = [ Mock.of<ShlinkDomain>({ domain: 'foo' }), Mock.of<ShlinkDomain>({ domain: 'boo' }) ];
|
const dispatch = jest.fn();
|
||||||
const domains = [ ...filteredDomains, Mock.of<ShlinkDomain>({ domain: 'bar' }) ];
|
const getState = jest.fn();
|
||||||
|
const listDomains = jest.fn();
|
||||||
|
const health = jest.fn();
|
||||||
|
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ listDomains, health });
|
||||||
|
const filteredDomains = [
|
||||||
|
Mock.of<Domain>({ domain: 'foo', status: 'validating' }),
|
||||||
|
Mock.of<Domain>({ domain: 'boo', status: 'validating' }),
|
||||||
|
];
|
||||||
|
const domains = [ ...filteredDomains, Mock.of<Domain>({ domain: 'bar', status: 'validating' }) ];
|
||||||
|
|
||||||
|
beforeEach(jest.clearAllMocks);
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
const action = (type: string, args: Partial<DomainsCombinedAction> = {}) => Mock.of<DomainsCombinedAction>(
|
const action = (type: string, args: Partial<DomainsCombinedAction> = {}) => Mock.of<DomainsCombinedAction>(
|
||||||
@@ -66,16 +82,23 @@ describe('domainsListReducer', () => {
|
|||||||
filteredDomains: filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
|
filteredDomains: filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ 'foo' ],
|
||||||
|
[ 'bar' ],
|
||||||
|
[ 'does_not_exist' ],
|
||||||
|
])('replaces status on proper domain on VALIDATE_DOMAIN', (domain) => {
|
||||||
|
expect(reducer(
|
||||||
|
Mock.of<DomainsList>({ domains, filteredDomains }),
|
||||||
|
action(VALIDATE_DOMAIN, { domain, status: 'valid' }),
|
||||||
|
)).toEqual({
|
||||||
|
domains: domains.map(replaceStatusOnDomain(domain, 'valid')),
|
||||||
|
filteredDomains: filteredDomains.map(replaceStatusOnDomain(domain, 'valid')),
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('listDomains', () => {
|
describe('listDomains', () => {
|
||||||
const dispatch = jest.fn();
|
|
||||||
const getState = jest.fn();
|
|
||||||
const listDomains = jest.fn();
|
|
||||||
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ listDomains });
|
|
||||||
|
|
||||||
beforeEach(jest.clearAllMocks);
|
|
||||||
|
|
||||||
it('dispatches error when loading domains fails', async () => {
|
it('dispatches error when loading domains fails', async () => {
|
||||||
listDomains.mockRejectedValue(new Error('error'));
|
listDomains.mockRejectedValue(new Error('error'));
|
||||||
|
|
||||||
@@ -108,4 +131,61 @@ describe('domainsListReducer', () => {
|
|||||||
expect(filterDomainsAction(searchTerm)).toEqual({ type: FILTER_DOMAINS, searchTerm });
|
expect(filterDomainsAction(searchTerm)).toEqual({ type: FILTER_DOMAINS, searchTerm });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('checkDomainHealth', () => {
|
||||||
|
const domain = 'example.com';
|
||||||
|
|
||||||
|
it('dispatches invalid status when selected server does not have all required data', async () => {
|
||||||
|
getState.mockReturnValue(Mock.of<ShlinkState>({
|
||||||
|
selectedServer: Mock.all<SelectedServer>(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(getState).toHaveBeenCalledTimes(1);
|
||||||
|
expect(health).not.toHaveBeenCalled();
|
||||||
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches invalid status when health endpoint returns an error', async () => {
|
||||||
|
getState.mockReturnValue(Mock.of<ShlinkState>({
|
||||||
|
selectedServer: Mock.of<ServerData>({
|
||||||
|
url: 'https://myerver.com',
|
||||||
|
apiKey: '123',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
health.mockRejectedValue({});
|
||||||
|
|
||||||
|
await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(getState).toHaveBeenCalledTimes(1);
|
||||||
|
expect(health).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ 'pass', 'valid' ],
|
||||||
|
[ 'fail', 'invalid' ],
|
||||||
|
])('dispatches proper status based on status returned from health endpoint', async (
|
||||||
|
healthStatus,
|
||||||
|
expectedStatus,
|
||||||
|
) => {
|
||||||
|
getState.mockReturnValue(Mock.of<ShlinkState>({
|
||||||
|
selectedServer: Mock.of<ServerData>({
|
||||||
|
url: 'https://myerver.com',
|
||||||
|
apiKey: '123',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
health.mockResolvedValue({ status: healthStatus });
|
||||||
|
|
||||||
|
await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(getState).toHaveBeenCalledTimes(1);
|
||||||
|
expect(health).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: expectedStatus });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
13
test/utils/helpers/uri.test.ts
Normal file
13
test/utils/helpers/uri.test.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { replaceAuthorityFromUri } from '../../../src/utils/helpers/uri';
|
||||||
|
|
||||||
|
describe('uri-helper', () => {
|
||||||
|
describe('replaceAuthorityFromUri', () => {
|
||||||
|
it.each([
|
||||||
|
[ 'http://something.com/foo/bar', 'www.new.to', 'http://www.new.to/foo/bar' ],
|
||||||
|
[ 'https://www.authori.ty:8000/', 'doma.in', 'https://doma.in/' ],
|
||||||
|
[ 'http://localhost:8080/this/is-a-long/path', 'somewhere:8888', 'http://somewhere:8888/this/is-a-long/path' ],
|
||||||
|
])('replaces authority as expected', (uri, newAuthority, expectedResult) => {
|
||||||
|
expect(replaceAuthorityFromUri(uri, newAuthority)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user