mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-14 11:33:51 +00:00
Extended domainsList reducer, adding functionality to verify domains statuses
This commit is contained in:
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;
|
||||||
|
}
|
||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user